[{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/next.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/postcss.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/scripts/bundle-monitor.mjs","messages":[{"ruleId":"no-unused-vars","severity":2,"message":"'PERFORMANCE_BUDGET' is assigned a value but never used.","line":22,"column":7,"nodeType":"Identifier","messageId":"unusedVar","endLine":22,"endColumn":25,"suggestions":[{"messageId":"removeVar","data":{"varName":"PERFORMANCE_BUDGET"},"fix":{"range":[514,787],"text":""},"desc":"Remove unused variable 'PERFORMANCE_BUDGET'."}]},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":45,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":45,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'process' is not defined.","line":46,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":46,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":79,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":79,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'process' is not defined.","line":80,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":80,"endColumn":14},{"ruleId":"no-unused-vars","severity":2,"message":"'error' is defined but never used.","line":96,"column":14,"nodeType":"Identifier","messageId":"unusedVar","endLine":96,"endColumn":19},{"ruleId":"no-unused-vars","severity":2,"message":"'issues' is defined but never used.","line":142,"column":37,"nodeType":"Identifier","messageId":"unusedVar","endLine":142,"endColumn":43,"suggestions":[{"messageId":"removeVar","data":{"varName":"issues"},"fix":{"range":[3870,3878],"text":""},"desc":"Remove unused variable 'issues'."}]},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":190,"column":9,"nodeType":"Identifier","messageId":"undef","endLine":190,"endColumn":16},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":202,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":202,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":204,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":204,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":230,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":230,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":255,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":255,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'process' is not defined.","line":256,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":256,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":258,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":258,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":269,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":269,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":270,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":270,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":271,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":271,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":272,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":272,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":277,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":277,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":283,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":283,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":284,"column":5,"nodeType":"Identifier","messageId":"undef","endLine":284,"endColumn":12},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":289,"column":9,"nodeType":"Identifier","messageId":"undef","endLine":289,"endColumn":16},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":294,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":294,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":295,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":295,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":298,"column":9,"nodeType":"Identifier","messageId":"undef","endLine":298,"endColumn":16},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":304,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":304,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":305,"column":7,"nodeType":"Identifier","messageId":"undef","endLine":305,"endColumn":14},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":307,"column":9,"nodeType":"Identifier","messageId":"undef","endLine":307,"endColumn":16},{"ruleId":"no-undef","severity":2,"message":"'console' is not defined.","line":309,"column":39,"nodeType":"Identifier","messageId":"undef","endLine":309,"endColumn":46}],"suppressedMessages":[],"errorCount":29,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"#!/usr/bin/env node\n\n/**\n * Bundle size monitoring script\n * Analyzes bundle size and reports on performance metrics\n */\n\nimport { readFileSync, writeFileSync, existsSync } from \"fs\";\nimport { join, dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst BUNDLE_SIZE_LIMIT = {\n // Size limits in KB\n total: 1000, // 1MB total\n individual: 250, // 250KB per chunk\n vendor: 500, // 500KB for vendor chunks\n};\n\nconst PERFORMANCE_BUDGET = {\n // Performance budget thresholds\n fcp: 1800, // First Contentful Paint (ms)\n lcp: 2500, // Largest Contentful Paint (ms)\n fid: 100, // First Input Delay (ms)\n cls: 0.1, // Cumulative Layout Shift\n ttfb: 800, // Time to First Byte (ms)\n};\n\nclass BundleMonitor {\n constructor() {\n this.projectRoot = join(__dirname, \"..\");\n this.buildDir = join(this.projectRoot, \".next\");\n this.reportFile = join(this.projectRoot, \"bundle-report.json\");\n }\n\n /**\n * Analyze bundle size from Next.js build output\n */\n analyzeBundleSize() {\n const buildManifest = join(this.buildDir, \"build-manifest.json\");\n\n if (!existsSync(buildManifest)) {\n console.error('Build manifest not found. Run \"npm run build\" first.');\n process.exit(1);\n }\n\n try {\n const manifest = JSON.parse(readFileSync(buildManifest, \"utf8\"));\n const chunks = [];\n let totalSize = 0;\n\n // Analyze JavaScript chunks\n Object.entries(manifest.pages || {}).forEach(([page, files]) => {\n files.forEach(file => {\n if (file.endsWith(\".js\")) {\n const filePath = join(this.buildDir, \"static\", file);\n if (existsSync(filePath)) {\n const stats = this.getFileStats(filePath);\n chunks.push({\n page,\n file,\n size: stats.size,\n gzippedSize: stats.gzippedSize,\n });\n totalSize += stats.size;\n }\n }\n });\n });\n\n return {\n totalSize,\n chunks,\n timestamp: Date.now(),\n };\n } catch (error) {\n console.error(\"Error analyzing bundle:\", error.message);\n process.exit(1);\n }\n }\n\n /**\n * Get file statistics including gzipped size\n */\n getFileStats(filePath) {\n try {\n const content = readFileSync(filePath);\n const size = content.length;\n\n // Estimate gzipped size (rough approximation)\n const gzippedSize = Math.round(size * 0.3); // Typical compression ratio\n\n return { size, gzippedSize };\n } catch (error) {\n return { size: 0, gzippedSize: 0 };\n }\n }\n\n /**\n * Check if bundle sizes are within limits\n */\n checkBundleLimits(analysis) {\n const issues = [];\n\n // Check total size\n const totalSizeKB = analysis.totalSize / 1024;\n if (totalSizeKB > BUNDLE_SIZE_LIMIT.total) {\n issues.push({\n type: \"total_size\",\n message: `Total bundle size (${totalSizeKB.toFixed(1)}KB) exceeds limit (${BUNDLE_SIZE_LIMIT.total}KB)`,\n severity: \"error\",\n });\n }\n\n // Check individual chunks\n analysis.chunks.forEach(chunk => {\n const sizeKB = chunk.size / 1024;\n\n if (sizeKB > BUNDLE_SIZE_LIMIT.individual) {\n const isVendor = chunk.file.includes(\"vendor\") || chunk.file.includes(\"node_modules\");\n const limit = isVendor ? BUNDLE_SIZE_LIMIT.vendor : BUNDLE_SIZE_LIMIT.individual;\n\n if (sizeKB > limit) {\n issues.push({\n type: \"chunk_size\",\n message: `Chunk ${chunk.file} (${sizeKB.toFixed(1)}KB) exceeds limit (${limit}KB)`,\n severity: \"warning\",\n chunk: chunk.file,\n });\n }\n }\n });\n\n return issues;\n }\n\n /**\n * Generate recommendations for bundle optimization\n */\n generateRecommendations(analysis, issues) {\n const recommendations = [];\n\n // Large chunks recommendations\n const largeChunks = analysis.chunks\n .filter(chunk => chunk.size / 1024 > 100)\n .sort((a, b) => b.size - a.size);\n\n if (largeChunks.length > 0) {\n recommendations.push({\n type: \"code_splitting\",\n message: \"Consider implementing code splitting for large chunks\",\n chunks: largeChunks.slice(0, 5).map(c => c.file),\n });\n }\n\n // Vendor chunk recommendations\n const vendorChunks = analysis.chunks.filter(\n chunk => chunk.file.includes(\"vendor\") || chunk.file.includes(\"framework\")\n );\n\n if (vendorChunks.some(chunk => chunk.size / 1024 > 300)) {\n recommendations.push({\n type: \"vendor_optimization\",\n message: \"Consider optimizing vendor chunks or using dynamic imports\",\n });\n }\n\n // Duplicate code detection (simplified)\n const pageChunks = analysis.chunks.filter(chunk => chunk.page !== \"_app\");\n if (pageChunks.length > 10) {\n recommendations.push({\n type: \"common_chunks\",\n message: \"Consider extracting common code into shared chunks\",\n });\n }\n\n return recommendations;\n }\n\n /**\n * Load previous report for comparison\n */\n loadPreviousReport() {\n if (existsSync(this.reportFile)) {\n try {\n return JSON.parse(readFileSync(this.reportFile, \"utf8\"));\n } catch (error) {\n console.warn(\"Could not load previous report:\", error.message);\n }\n }\n return null;\n }\n\n /**\n * Save current report\n */\n saveReport(report) {\n try {\n writeFileSync(this.reportFile, JSON.stringify(report, null, 2));\n console.log(`Report saved to ${this.reportFile}`);\n } catch (error) {\n console.error(\"Could not save report:\", error.message);\n }\n }\n\n /**\n * Compare with previous report\n */\n compareWithPrevious(current, previous) {\n if (!previous) return null;\n\n const currentTotal = current.analysis.totalSize;\n const previousTotal = previous.analysis.totalSize;\n const sizeDiff = currentTotal - previousTotal;\n const percentChange = (sizeDiff / previousTotal) * 100;\n\n return {\n sizeDiff,\n percentChange,\n isRegression: sizeDiff > 10240, // 10KB threshold\n };\n }\n\n /**\n * Generate and display report\n */\n run() {\n console.log(\"🔍 Analyzing bundle size...\\n\");\n\n const analysis = this.analyzeBundleSize();\n const issues = this.checkBundleLimits(analysis);\n const recommendations = this.generateRecommendations(analysis, issues);\n const previous = this.loadPreviousReport();\n const comparison = this.compareWithPrevious({ analysis }, previous);\n\n const report = {\n timestamp: Date.now(),\n analysis,\n issues,\n recommendations,\n comparison,\n };\n\n // Display results\n this.displayReport(report);\n\n // Save report\n this.saveReport(report);\n\n // Exit with error code if there are critical issues\n const hasErrors = issues.some(issue => issue.severity === \"error\");\n if (hasErrors) {\n console.log(\"\\n❌ Bundle analysis failed due to critical issues.\");\n process.exit(1);\n } else {\n console.log(\"\\n✅ Bundle analysis completed successfully.\");\n }\n }\n\n /**\n * Display formatted report\n */\n displayReport(report) {\n const { analysis, issues, recommendations, comparison } = report;\n\n // Bundle size summary\n console.log(\"📊 Bundle Size Summary\");\n console.log(\"─\".repeat(50));\n console.log(`Total Size: ${(analysis.totalSize / 1024).toFixed(1)}KB`);\n console.log(`Chunks: ${analysis.chunks.length}`);\n\n if (comparison) {\n const sign = comparison.sizeDiff > 0 ? \"+\" : \"\";\n const color = comparison.isRegression ? \"\\x1b[31m\" : \"\\x1b[32m\";\n console.log(\n `Change: ${color}${sign}${(comparison.sizeDiff / 1024).toFixed(1)}KB (${comparison.percentChange.toFixed(1)}%)\\x1b[0m`\n );\n }\n\n // Top chunks\n console.log(\"\\n📦 Largest Chunks\");\n console.log(\"─\".repeat(50));\n analysis.chunks\n .sort((a, b) => b.size - a.size)\n .slice(0, 10)\n .forEach(chunk => {\n console.log(`${(chunk.size / 1024).toFixed(1)}KB - ${chunk.file}`);\n });\n\n // Issues\n if (issues.length > 0) {\n console.log(\"\\n⚠️ Issues Found\");\n console.log(\"─\".repeat(50));\n issues.forEach(issue => {\n const icon = issue.severity === \"error\" ? \"❌\" : \"⚠️ \";\n console.log(`${icon} ${issue.message}`);\n });\n }\n\n // Recommendations\n if (recommendations.length > 0) {\n console.log(\"\\n💡 Recommendations\");\n console.log(\"─\".repeat(50));\n recommendations.forEach(rec => {\n console.log(`• ${rec.message}`);\n if (rec.chunks) {\n rec.chunks.forEach(chunk => console.log(` - ${chunk}`));\n }\n });\n }\n }\n}\n\n// Run the monitor\nconst monitor = new BundleMonitor();\nmonitor.run();\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/scripts/dev-prep.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/account/profile/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/billing/invoices/[id]/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/billing/invoices/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/billing/payments/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/catalog/internet/configure/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/catalog/internet/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/catalog/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/catalog/sim/configure/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/catalog/sim/page.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'Link' is defined but never used.","line":4,"column":8,"nodeType":null,"messageId":"unusedVar","endLine":4,"endColumn":12},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"The 'plans' logical expression could make the dependencies of useEffect Hook (at line 141) change on every render. To fix this, wrap the initialization of 'plans' in its own useMemo() Hook.","line":132,"column":9,"nodeType":"VariableDeclarator","endLine":132,"endColumn":34}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport Link from \"next/link\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport {\n DevicePhoneMobileIcon,\n CurrencyYenIcon,\n CheckIcon,\n InformationCircleIcon,\n UsersIcon,\n PhoneIcon,\n GlobeAltIcon,\n ArrowLeftIcon,\n} from \"@heroicons/react/24/outline\";\nimport { useSimCatalog } from \"@/features/catalog/hooks\";\n\nimport { SimPlan } from \"@/shared/types/catalog.types\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { Button } from \"@/components/ui/button\";\n\ninterface PlansByType {\n DataOnly: SimPlan[];\n DataSmsVoice: SimPlan[];\n VoiceOnly: SimPlan[];\n}\n\nfunction PlanTypeSection({\n title,\n description,\n icon,\n plans,\n showFamilyDiscount,\n}: {\n title: string;\n description: string;\n icon: React.ReactNode;\n plans: SimPlan[];\n showFamilyDiscount: boolean;\n}) {\n if (plans.length === 0) return null;\n\n // Separate regular and family plans\n const regularPlans = plans.filter(p => !p.hasFamilyDiscount);\n const familyPlans = plans.filter(p => p.hasFamilyDiscount);\n\n return (\n
\n
\n {icon}\n
\n

{title}

\n

{description}

\n
\n
\n\n {/* Regular Plans */}\n
\n {regularPlans.map(plan => (\n \n ))}\n
\n\n {/* Family Discount Plans */}\n {showFamilyDiscount && familyPlans.length > 0 && (\n <>\n
\n \n

Family Discount Options

\n \n You qualify!\n \n
\n
\n {familyPlans.map(plan => (\n \n ))}\n
\n \n )}\n
\n );\n}\n\nfunction PlanCard({ plan, isFamily }: { plan: SimPlan; isFamily: boolean }) {\n return (\n \n
\n
\n
\n \n {plan.dataSize}\n
\n {isFamily && (\n
\n \n \n Family\n \n
\n )}\n
\n
\n\n
\n
\n \n \n {plan.monthlyPrice?.toLocaleString()}\n \n /month\n
\n {isFamily && (\n
Discounted price
\n )}\n
\n\n
\n

{plan.description}

\n
\n\n \n
\n );\n}\n\nexport default function SimPlansPage() {\n const { data, isLoading, error } = useSimCatalog();\n const plans = data?.plans || [];\n const [hasExistingSim, setHasExistingSim] = useState(false);\n const [activeTab, setActiveTab] = useState<\"data-voice\" | \"data-only\" | \"voice-only\">(\n \"data-voice\"\n );\n\n useEffect(() => {\n // Check if any plans have family discount (indicates user has existing SIM)\n setHasExistingSim(plans.some(p => p.hasFamilyDiscount));\n }, [plans]);\n\n if (isLoading) {\n return (\n }\n >\n
\n \n
\n \n );\n }\n\n if (error) {\n const errorMessage = error instanceof Error ? error.message : \"An unexpected error occurred\";\n return (\n }\n >\n
\n
Failed to load SIM plans
\n
{errorMessage}
\n \n
\n \n );\n }\n\n // Group plans by type\n const plansByType: PlansByType = plans.reduce(\n (acc, plan) => {\n acc[plan.planType].push(plan);\n return acc;\n },\n {\n DataOnly: [] as SimPlan[],\n DataSmsVoice: [] as SimPlan[],\n VoiceOnly: [] as SimPlan[],\n }\n );\n\n return (\n }\n >\n
\n {/* Navigation */}\n
\n \n
\n\n
\n

Choose Your SIM Plan

\n

\n Wide range of data options and voice plans with both physical SIM and eSIM options.\n

\n
\n {/* Family Discount Banner */}\n {hasExistingSim && (\n
\n
\n
\n \n
\n
\n

\n 🎉 Family Discount Available!\n

\n

\n You have existing SIM services, so you qualify for family discount pricing on\n additional lines.\n

\n
\n
\n \n Reduced monthly pricing\n
\n
\n \n Same great features\n
\n
\n \n Easy to manage\n
\n
\n
\n
\n
\n )}\n\n {/* Tab Navigation */}\n
\n
\n \n
\n
\n\n {/* Tab Content */}\n
\n \n {activeTab === \"data-voice\" && (\n }\n plans={plansByType.DataSmsVoice}\n showFamilyDiscount={hasExistingSim}\n />\n )}\n
\n\n \n {activeTab === \"data-only\" && (\n }\n plans={plansByType.DataOnly}\n showFamilyDiscount={hasExistingSim}\n />\n )}\n
\n\n \n {activeTab === \"voice-only\" && (\n }\n plans={plansByType.VoiceOnly}\n showFamilyDiscount={hasExistingSim}\n />\n )}\n \n \n\n {/* Features Section */}\n
\n

\n Plan Features & Terms\n

\n
\n
\n \n
\n
3-Month Contract
\n
Minimum 3 billing months
\n
\n
\n
\n \n
\n
First Month Free
\n
Basic fee waived initially
\n
\n
\n
\n \n
\n
5G Network
\n
High-speed coverage
\n
\n
\n
\n \n
\n
eSIM Support
\n
Digital activation
\n
\n
\n
\n \n
\n
Family Discounts
\n
Multi-line savings
\n
\n
\n
\n \n
\n
Plan Switching
\n
Free data plan changes
\n
\n
\n
\n
\n\n {/* Info Section */}\n
\n
\n \n
\n
Important Terms & Conditions
\n
\n
\n
\n
\n
\n
Contract Period
\n

\n Minimum 3 full billing months required. First month (sign-up to end of month) is\n free and doesn't count toward contract.\n

\n
\n
\n
Billing Cycle
\n

\n Monthly billing from 1st to end of month. Regular billing starts on 1st of\n following month after sign-up.\n

\n
\n
\n
Cancellation
\n

\n Can be requested online after 3rd month. Service terminates at end of billing\n cycle.\n

\n
\n
\n
\n
\n
Plan Changes
\n

\n Data plan switching is free and takes effect next month. Voice plan changes\n require new SIM and cancellation policies apply.\n

\n
\n
\n
Calling/SMS Charges
\n

\n Pay-per-use charges apply separately. Billed 5-6 weeks after usage within billing\n cycle.\n

\n
\n
\n
SIM Replacement
\n

\n Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards.\n

\n
\n
\n
\n
\n \n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/catalog/vpn/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/checkout/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/dashboard/page.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'useDashboardStore' is defined but never used.","line":9,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":9,"endColumn":27},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'CreditCardIcon' is defined but never used.","line":14,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":14,"endColumn":17},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ExclamationTriangleIcon' is defined but never used.","line":17,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":17,"endColumn":26},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'PlusIcon' is defined but never used.","line":19,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":19,"endColumn":11},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ArrowTrendingUpIcon' is defined but never used.","line":21,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":21,"endColumn":22},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'BellIcon' is defined but never used.","line":23,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":23,"endColumn":11},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ClipboardDocumentListIcon' is defined but never used.","line":24,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":24,"endColumn":28},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `;`","line":56,"column":9,"nodeType":null,"messageId":"delete","endLine":56,"endColumn":10,"fix":{"range":[2157,2158],"text":""}},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'truncateName' is defined but never used.","line":298,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":298,"endColumn":22},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":303,"column":85,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":303,"endColumn":88,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[12711,12714],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[12711,12714],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `{ nextInvoice?: { id: number; } | null | undefined; stats?: { unpaidInvoices?: number | undefined; openCases?: number | undefined; } | undefined; }`.","line":307,"column":40,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":307,"endColumn":47}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":9,"fixableErrorCount":0,"fixableWarningCount":1,"source":"\"use client\";\nimport { logger } from \"@/lib/logger\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { useDashboardSummary } from \"@/features/dashboard/hooks/useDashboardSummary\";\nimport { useDashboardStore } from \"@/features/dashboard/stores/dashboard.store\";\nimport { generateDashboardTasks } from \"@/features/dashboard/utils/dashboard.utils\";\n\nimport type { Activity } from \"@customer-portal/shared\";\nimport {\n CreditCardIcon,\n ServerIcon,\n ChatBubbleLeftRightIcon,\n ExclamationTriangleIcon,\n ChevronRightIcon,\n PlusIcon,\n DocumentTextIcon,\n ArrowTrendingUpIcon,\n CalendarDaysIcon,\n BellIcon,\n ClipboardDocumentListIcon,\n} from \"@heroicons/react/24/outline\";\nimport {\n CreditCardIcon as CreditCardIconSolid,\n ServerIcon as ServerIconSolid,\n ChatBubbleLeftRightIcon as ChatBubbleLeftRightIconSolid,\n ClipboardDocumentListIcon as ClipboardDocumentListIconSolid,\n} from \"@heroicons/react/24/solid\";\nimport { format, formatDistanceToNow } from \"date-fns\";\nimport { StatCard, QuickAction, ActivityFeed } from \"@/features/dashboard/components\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { ErrorState } from \"@/components/ui/error-state\";\nimport { formatCurrency, getCurrencyLocale } from \"@/utils/currency\";\n\nexport default function DashboardPage() {\n const router = useRouter();\n const { user, isAuthenticated, isLoading: authLoading } = useAuthStore();\n const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();\n\n const [paymentLoading, setPaymentLoading] = useState(false);\n const [paymentError, setPaymentError] = useState(null);\n\n // Handle Pay Now functionality\n const handlePayNow = (invoiceId: number) => {\n setPaymentLoading(true);\n setPaymentError(null);\n\n void (async () => {\n try {\n const { BillingService } = await import(\"@/features/billing/services/billing.service\");\n const ssoLink = await BillingService.createInvoiceSsoLink({ invoiceId, target: \"pay\" });\n // Centralized SSO link opening\n ;(await import(\"@/lib/utils/sso\")).openSsoLink(ssoLink.url, { newTab: true });\n } catch (error) {\n logger.error(error, \"Failed to create payment link\");\n setPaymentError(error instanceof Error ? error.message : \"Failed to open payment page\");\n } finally {\n setPaymentLoading(false);\n }\n })();\n };\n\n // Handle activity item clicks\n const handleActivityClick = (activity: Activity) => {\n if (activity.type === \"invoice_created\" || activity.type === \"invoice_paid\") {\n // Use the related invoice ID for navigation\n if (activity.relatedId) {\n router.push(`/billing/invoices/${activity.relatedId}`);\n }\n }\n };\n\n if (authLoading || summaryLoading || !isAuthenticated) {\n return (\n
\n
\n \n

Loading dashboard...

\n
\n
\n );\n }\n\n // Handle error state\n if (error) {\n return (\n \n );\n }\n\n return (\n <>\n
\n
\n {/* Modern Header */}\n
\n
\n
\n
\n

\n Welcome back, {user?.firstName || user?.email?.split(\"@\")[0] || \"User\"}!\n

\n {/* Tasks chip */}\n \n
\n
Portal / Dashboard
\n
\n {/* No duplicate page-level CTAs here per guidelines */}\n
\n
\n\n {/* Modern Stats Grid */}\n
\n \n 0\n ? \"from-amber-500 to-orange-500\"\n : \"from-gray-500 to-gray-600\"\n }\n href=\"/billing/invoices\"\n zeroHint={{ text: \"Set up auto-pay\", href: \"/billing/payments\" }}\n loading={summaryLoading}\n error={!!error}\n />\n \n 0\n ? \"from-blue-500 to-cyan-500\"\n : \"from-gray-500 to-gray-600\"\n }\n href=\"/support/cases\"\n zeroHint={{ text: \"Open a ticket\", href: \"/support/new\" }}\n loading={summaryLoading}\n error={!!error}\n />\n
\n\n
\n {/* Main Content Area */}\n
\n {/* Upcoming Payment - compressed attention banner */}\n {summary?.nextInvoice && (\n \n
\n
\n
\n \n
\n
\n
\n
\n Upcoming Payment\n \n Invoice #{summary.nextInvoice.id}\n \n \n Due{\" \"}\n {formatDistanceToNow(new Date(summary.nextInvoice.dueDate), {\n addSuffix: true,\n })}\n \n
\n
\n {formatCurrency(summary.nextInvoice.amount, {\n currency: summary.nextInvoice.currency || \"JPY\",\n locale: getCurrencyLocale(summary.nextInvoice.currency || \"JPY\"),\n })}\n
\n
\n Exact due date:{\" \"}\n {format(new Date(summary.nextInvoice.dueDate), \"MMMM d, yyyy\")}\n
\n
\n
\n handlePayNow(summary.nextInvoice!.id)}\n disabled={paymentLoading}\n className=\"inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n {paymentLoading ? (\n \n Opening...\n \n ) : null}\n {paymentLoading ? \"Opening Payment...\" : \"Pay Now\"}\n {!paymentLoading && }\n \n \n View invoice\n \n
\n
\n
\n )}\n\n {/* Payment Error Display */}\n {paymentError && (\n setPaymentError(null)}\n retryLabel=\"Dismiss\"\n />\n )}\n\n {/* Recent Activity - filtered list */}\n \n
\n\n {/* Sidebar */}\n
\n {/* Quick Actions - simplified */}\n
\n
\n

Quick Actions

\n
\n
\n \n \n \n
\n
\n
\n
\n
\n \n \n );\n}\n\n// Helpers and small components (local to dashboard)\nfunction truncateName(name: string, len = 28) {\n if (name.length <= len) return name;\n return name.slice(0, Math.max(0, len - 1)) + \"…\";\n}\n\nfunction TasksChip({ summaryLoading, summary }: { summaryLoading: boolean; summary: any }) {\n const router = useRouter();\n if (summaryLoading) return null;\n\n const tasks = generateDashboardTasks(summary);\n const count = tasks.length;\n if (count === 0) return null;\n\n return (\n {\n const first = tasks[0];\n if (first.href.startsWith(\"#\")) {\n const el = document.querySelector(first.href);\n if (el) el.scrollIntoView({ behavior: \"smooth\", block: \"start\" });\n } else {\n router.push(first.href);\n }\n }}\n className=\"inline-flex items-center rounded-full bg-blue-50 text-blue-700 px-2.5 py-1 text-xs font-medium hover:bg-blue-100 transition-colors duration-200\"\n title={tasks.map(t => t.label).join(\" • \")}\n >\n {count} task{count === 1 ? \"\" : \"s\"}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/orders/[id]/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/orders/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/subscriptions/[id]/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/subscriptions/[id]/sim/cancel/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/subscriptions/[id]/sim/change-plan/page.tsx","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":49,"column":17,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":49,"endColumn":20,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1667,1670],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1667,1670],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":83,"column":24,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":83,"endColumn":32}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useState, useMemo } from \"react\";\nimport Link from \"next/link\";\nimport { useParams } from \"next/navigation\";\nimport { simActionsService } from \"@/features/subscriptions/services/sim-actions.service\";\n\nconst PLAN_CODES = [\"PASI_5G\", \"PASI_10G\", \"PASI_25G\", \"PASI_50G\"] as const;\ntype PlanCode = (typeof PLAN_CODES)[number];\nconst PLAN_LABELS: Record = {\n PASI_5G: \"5GB\",\n PASI_10G: \"10GB\",\n PASI_25G: \"25GB\",\n PASI_50G: \"50GB\",\n};\n\nexport default function SimChangePlanPage() {\n const params = useParams();\n const subscriptionId = parseInt(params.id as string);\n const [currentPlanCode] = useState(\"\");\n const [newPlanCode, setNewPlanCode] = useState<\"\" | PlanCode>(\"\");\n const [assignGlobalIp, setAssignGlobalIp] = useState(false);\n const [scheduledAt, setScheduledAt] = useState(\"\");\n const [message, setMessage] = useState(null);\n const [error, setError] = useState(null);\n const [loading, setLoading] = useState(false);\n\n const options = useMemo(\n () => (PLAN_CODES as readonly PlanCode[]).filter(c => c !== (currentPlanCode as PlanCode)),\n [currentPlanCode]\n );\n\n const submit = async (e: React.FormEvent) => {\n e.preventDefault();\n if (!newPlanCode) {\n setError(\"Please select a new plan\");\n return;\n }\n setLoading(true);\n setMessage(null);\n setError(null);\n try {\n await simActionsService.changePlan(subscriptionId, {\n newPlanCode,\n assignGlobalIp,\n scheduledAt: scheduledAt ? scheduledAt.replace(/-/g, \"\") : undefined,\n });\n setMessage(\"Plan change submitted successfully\");\n } catch (e: any) {\n setError(e instanceof Error ? e.message : \"Failed to change plan\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n
\n
\n \n ← Back to SIM Management\n \n
\n
\n

Change Plan

\n

\n Change Plan: Switch to a different data plan. Important: Plan changes must be requested\n before the 25th of the month. Changes will take effect on the 1st of the following month.\n

\n {message && (\n
\n {message}\n
\n )}\n {error && (\n
\n {error}\n
\n )}\n\n
\n
\n \n setNewPlanCode(e.target.value as PlanCode)}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-md\"\n >\n \n {options.map(code => (\n \n ))}\n \n
\n\n
\n setAssignGlobalIp(e.target.checked)}\n className=\"h-4 w-4 text-blue-600 border-gray-300 rounded\"\n />\n \n
\n\n
\n \n setScheduledAt(e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-md\"\n />\n
\n\n
\n \n {loading ? \"Processing…\" : \"Submit Plan Change\"}\n \n \n Back\n \n
\n
\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/subscriptions/[id]/sim/reissue/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/subscriptions/[id]/sim/top-up/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/subscriptions/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/support/cases/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/(portal)/support/new/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/api/auth/login/route.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/api/auth/signup/route.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/api/auth/validate-signup/route.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/api/health/route.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/forgot-password/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/link-whmcs/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/login/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/reset-password/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/set-password/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/auth/signup/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/app/page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/auth/session-timeout-warning.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/DataTable/DataTable.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/DataTable/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/DetailHeader/DetailHeader.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·title,·subtitle,·status,·leftIcon,·actions,·className,·meta·` with `⏎··title,⏎··subtitle,⏎··status,⏎··leftIcon,⏎··actions,⏎··className,⏎··meta,⏎`","line":18,"column":31,"nodeType":null,"messageId":"replace","endLine":18,"endColumn":92,"fix":{"range":[465,526],"text":"\n title,\n subtitle,\n status,\n leftIcon,\n actions,\n className,\n meta,\n"}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"\"use client\";\n\nimport React from \"react\";\nimport { StatusPill } from \"@/components/ui/status-pill\";\n\ntype Variant = \"success\" | \"warning\" | \"error\" | \"neutral\" | \"info\";\n\ninterface DetailHeaderProps {\n title: string;\n subtitle?: string;\n status?: { label: string; variant: Variant };\n leftIcon?: React.ReactNode;\n actions?: React.ReactNode;\n className?: string;\n meta?: React.ReactNode; // optional metadata row under header\n}\n\nexport function DetailHeader({ title, subtitle, status, leftIcon, actions, className, meta }: DetailHeaderProps) {\n return (\n
\n
\n
\n {leftIcon}\n
\n

{title}

\n {subtitle &&

{subtitle}

}\n
\n
\n {status && }\n {actions}\n
\n {meta &&
{meta}
}\n
\n );\n}\n\nexport type { DetailHeaderProps };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/DetailHeader/index.ts","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":2,"column":1,"nodeType":null,"messageId":"delete","endLine":3,"endColumn":1,"fix":{"range":[32,33],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"export * from \"./DetailHeader\";\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/FormField/FormField.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/FormField/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/PaginationBar/PaginationBar.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·currentPage,·pageSize,·totalItems,·onPageChange,·className·` with `⏎··currentPage,⏎··pageSize,⏎··totalItems,⏎··onPageChange,⏎··className,⏎`","line":13,"column":32,"nodeType":null,"messageId":"replace","endLine":13,"endColumn":92,"fix":{"range":[235,295],"text":"\n currentPage,\n pageSize,\n totalItems,\n onPageChange,\n className,\n"}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `·`","line":39,"column":95,"nodeType":null,"messageId":"delete","endLine":39,"endColumn":96,"fix":{"range":[1639,1640],"text":""}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `·`","line":40,"column":99,"nodeType":null,"messageId":"delete","endLine":40,"endColumn":100,"fix":{"range":[1744,1745],"text":""}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·className=\"relative·z-0·inline-flex·rounded-md·shadow-sm·-space-x-px\"·aria-label=\"Pagination\"` with `⏎············className=\"relative·z-0·inline-flex·rounded-md·shadow-sm·-space-x-px\"⏎············aria-label=\"Pagination\"⏎··········`","line":45,"column":15,"nodeType":null,"messageId":"replace","endLine":45,"endColumn":109,"fix":{"range":[1879,1973],"text":"\n className=\"relative z-0 inline-flex rounded-md shadow-sm -space-x-px\"\n aria-label=\"Pagination\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":68,"column":1,"nodeType":null,"messageId":"delete","endLine":69,"endColumn":1,"fix":{"range":[2880,2881],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":5,"fixableErrorCount":0,"fixableWarningCount":5,"source":"\"use client\";\n\nimport React from \"react\";\n\ninterface PaginationBarProps {\n currentPage: number;\n pageSize: number;\n totalItems: number;\n onPageChange: (page: number) => void;\n className?: string;\n}\n\nexport function PaginationBar({ currentPage, pageSize, totalItems, onPageChange, className }: PaginationBarProps) {\n const totalPages = Math.max(1, Math.ceil(totalItems / pageSize));\n const canPrev = currentPage > 1;\n const canNext = currentPage < totalPages;\n\n return (\n
\n
\n onPageChange(Math.max(1, currentPage - 1))}\n disabled={!canPrev}\n className=\"relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Previous\n \n onPageChange(Math.min(totalPages, currentPage + 1))}\n disabled={!canNext}\n className=\"ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Next\n \n
\n
\n
\n

\n Showing {(currentPage - 1) * pageSize + 1} to {\" \"}\n {Math.min(currentPage * pageSize, totalItems)} of {\" \"}\n {totalItems} results\n

\n
\n
\n \n
\n
\n
\n );\n}\n\nexport type { PaginationBarProps };\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/PaginationBar/index.ts","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":2,"column":1,"nodeType":null,"messageId":"delete","endLine":3,"endColumn":1,"fix":{"range":[33,34],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"export * from \"./PaginationBar\";\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/SearchFilterBar/SearchFilterBar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/SearchFilterBar/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/error-boundary.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/lazy-component.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/lazy-wrapper.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/optimized-image.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/preloading-link.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/common/web-vitals-monitor.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'error' is defined but never used.","line":57,"column":16,"nodeType":null,"messageId":"unusedVar","endLine":57,"endColumn":21},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":63,"column":5,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":63,"endColumn":21,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[1641,1641],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[1641,1641],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":115,"column":13,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":115,"endColumn":38},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":115,"column":35,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":115,"endColumn":38,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3148,3151],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3148,3151],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .processingStart on an `any` value.","line":116,"column":22,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":116,"endColumn":37},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .startTime on an `any` value.","line":116,"column":52,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":116,"endColumn":61},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .processingStart on an `any` value.","line":117,"column":32,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":117,"endColumn":47},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .startTime on an `any` value.","line":117,"column":61,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":117,"endColumn":70},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":133,"column":22,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":133,"endColumn":25,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3738,3741],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3738,3741],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .hadRecentInput on an `any` value.","line":133,"column":27,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":133,"endColumn":41},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":134,"column":31,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":134,"endColumn":34,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3791,3794],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3791,3794],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .value on an `any` value.","line":134,"column":36,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":134,"endColumn":41},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a(n) `any` typed value.","line":184,"column":5,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":184,"endColumn":25},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":184,"column":16,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":184,"endColumn":19,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[5178,5181],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[5178,5181],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .gtag on an `any` value.","line":184,"column":21,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":184,"endColumn":25},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":209,"column":20,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":209,"endColumn":23,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[5729,5732],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[5729,5732],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any[]`.","line":229,"column":5,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":229,"endColumn":20}],"suppressedMessages":[],"errorCount":16,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useEffect } from \"react\";\n\ninterface WebVitalsMetric {\n name: string;\n value: number;\n rating: \"good\" | \"needs-improvement\" | \"poor\";\n delta: number;\n id: string;\n}\n\ninterface WebVitalsMonitorProps {\n onMetric?: (metric: WebVitalsMetric) => void;\n reportToAnalytics?: boolean;\n}\n\n/**\n * Web Vitals monitoring component\n * Measures and reports Core Web Vitals metrics\n */\nexport function WebVitalsMonitor({ onMetric, reportToAnalytics = true }: WebVitalsMonitorProps) {\n useEffect(() => {\n // Only run in browser\n if (typeof window === \"undefined\") return;\n\n const reportMetric = (metric: WebVitalsMetric) => {\n // Call custom handler\n onMetric?.(metric);\n\n // Report to analytics if enabled\n if (reportToAnalytics) {\n reportToAnalyticsService(metric);\n }\n\n // Log in development\n if (process.env.NODE_ENV === \"development\") {\n console.log(`Web Vital - ${metric.name}:`, {\n value: metric.value,\n rating: metric.rating,\n delta: metric.delta,\n });\n }\n };\n\n // Try to use web-vitals library if available\n const loadWebVitals = async () => {\n try {\n // Dynamic import to avoid bundling if not needed\n const { onCLS, onINP, onFCP, onLCP, onTTFB } = await import(\"web-vitals\");\n\n onCLS(reportMetric);\n onINP(reportMetric);\n onFCP(reportMetric);\n onLCP(reportMetric);\n onTTFB(reportMetric);\n } catch (error) {\n // Fallback to manual measurement if web-vitals is not available\n measureWithPerformanceAPI(reportMetric);\n }\n };\n\n loadWebVitals();\n }, [onMetric, reportToAnalytics]);\n\n return null; // This component doesn't render anything\n}\n\n/**\n * Fallback measurement using Performance API\n */\nfunction measureWithPerformanceAPI(reportMetric: (metric: WebVitalsMetric) => void) {\n if (!(\"PerformanceObserver\" in window)) return;\n\n // Measure FCP\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n const fcp = entries.find(entry => entry.name === \"first-contentful-paint\");\n if (fcp) {\n reportMetric({\n name: \"FCP\",\n value: fcp.startTime,\n rating:\n fcp.startTime <= 1800 ? \"good\" : fcp.startTime <= 3000 ? \"needs-improvement\" : \"poor\",\n delta: fcp.startTime,\n id: generateId(),\n });\n }\n }).observe({ entryTypes: [\"paint\"] });\n\n // Measure LCP\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n const lastEntry = entries[entries.length - 1];\n if (lastEntry) {\n reportMetric({\n name: \"LCP\",\n value: lastEntry.startTime,\n rating:\n lastEntry.startTime <= 2500\n ? \"good\"\n : lastEntry.startTime <= 4000\n ? \"needs-improvement\"\n : \"poor\",\n delta: lastEntry.startTime,\n id: generateId(),\n });\n }\n }).observe({ entryTypes: [\"largest-contentful-paint\"] });\n\n // Measure FID\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n entries.forEach(entry => {\n const eventEntry = entry as any; // Type assertion for processingStart\n if (eventEntry.processingStart && eventEntry.startTime) {\n const fid = eventEntry.processingStart - eventEntry.startTime;\n reportMetric({\n name: \"FID\",\n value: fid,\n rating: fid <= 100 ? \"good\" : fid <= 300 ? \"needs-improvement\" : \"poor\",\n delta: fid,\n id: generateId(),\n });\n }\n });\n }).observe({ entryTypes: [\"first-input\"] });\n\n // Measure CLS\n let clsValue = 0;\n new PerformanceObserver(list => {\n list.getEntries().forEach(entry => {\n if (!(entry as any).hadRecentInput) {\n clsValue += (entry as any).value;\n }\n });\n\n reportMetric({\n name: \"CLS\",\n value: clsValue,\n rating: clsValue <= 0.1 ? \"good\" : clsValue <= 0.25 ? \"needs-improvement\" : \"poor\",\n delta: clsValue,\n id: generateId(),\n });\n }).observe({ entryTypes: [\"layout-shift\"] });\n\n // Measure TTFB\n const navigation = performance.getEntriesByType(\"navigation\")[0];\n if (navigation) {\n const ttfb = navigation.responseStart - navigation.requestStart;\n reportMetric({\n name: \"TTFB\",\n value: ttfb,\n rating: ttfb <= 800 ? \"good\" : ttfb <= 1800 ? \"needs-improvement\" : \"poor\",\n delta: ttfb,\n id: generateId(),\n });\n }\n}\n\n/**\n * Report metrics to analytics service\n */\nfunction reportToAnalyticsService(metric: WebVitalsMetric) {\n // Store in localStorage for now (replace with actual analytics service)\n if (typeof window !== \"undefined\" && window.localStorage) {\n const key = `web_vitals_${metric.name.toLowerCase()}`;\n const data = {\n ...metric,\n timestamp: Date.now(),\n url: window.location.pathname,\n userAgent: navigator.userAgent,\n };\n\n try {\n localStorage.setItem(key, JSON.stringify(data));\n } catch (error) {\n console.warn(\"Failed to store web vitals metric:\", error);\n }\n }\n\n // Send to analytics service (example)\n if (typeof window !== \"undefined\" && \"gtag\" in window) {\n (window as any).gtag(\"event\", metric.name, {\n event_category: \"Web Vitals\",\n event_label: metric.id,\n value: Math.round(metric.value),\n custom_map: {\n metric_rating: metric.rating,\n },\n });\n }\n}\n\n/**\n * Generate unique ID for metrics\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * Hook for accessing Web Vitals data\n */\nexport function useWebVitals() {\n const getStoredMetrics = () => {\n if (typeof window === \"undefined\") return [];\n\n const metrics: any[] = [];\n const vitalsKeys = [\n \"web_vitals_fcp\",\n \"web_vitals_lcp\",\n \"web_vitals_fid\",\n \"web_vitals_cls\",\n \"web_vitals_ttfb\",\n ];\n\n vitalsKeys.forEach(key => {\n try {\n const stored = localStorage.getItem(key);\n if (stored) {\n metrics.push(JSON.parse(stored));\n }\n } catch (error) {\n console.warn(`Failed to parse stored metric ${key}:`, error);\n }\n });\n\n return metrics;\n };\n\n const clearStoredMetrics = () => {\n if (typeof window === \"undefined\") return;\n\n const vitalsKeys = [\n \"web_vitals_fcp\",\n \"web_vitals_lcp\",\n \"web_vitals_fid\",\n \"web_vitals_cls\",\n \"web_vitals_ttfb\",\n ];\n vitalsKeys.forEach(key => {\n localStorage.removeItem(key);\n });\n };\n\n return {\n getStoredMetrics,\n clearStoredMetrics,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/AuthLayout/AuthLayout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/AuthLayout/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/DashboardLayout/DashboardLayout.tsx","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":315,"column":9,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":315,"endColumn":12,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[10010,10013],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[10010,10013],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .firstName on an `any` value.","line":359,"column":20,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":359,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-call","severity":2,"message":"Unsafe call of a(n) `any` typed value.","line":359,"column":33,"nodeType":"MemberExpression","messageId":"unsafeCall","endLine":359,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .email on an `any` value.","line":359,"column":39,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":359,"endColumn":44},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [0] on an `any` value.","line":359,"column":57,"nodeType":"Literal","messageId":"unsafeMemberExpression","endLine":359,"endColumn":58}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\n\nimport { useState, useEffect, useMemo, memo } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { Logo } from \"@/components/ui/logo\";\nimport {\n HomeIcon,\n CreditCardIcon,\n ServerIcon,\n ChatBubbleLeftRightIcon,\n UserIcon,\n Bars3Icon,\n XMarkIcon,\n BellIcon,\n ArrowRightStartOnRectangleIcon,\n Squares2X2Icon,\n ClipboardDocumentListIcon,\n QuestionMarkCircleIcon,\n} from \"@heroicons/react/24/outline\";\nimport { useActiveSubscriptions } from \"@/features/subscriptions/hooks\";\nimport { SessionTimeoutWarning } from \"@/components/auth/session-timeout-warning\";\nimport type { Subscription } from \"@customer-portal/shared\";\n\ninterface DashboardLayoutProps {\n children: React.ReactNode;\n}\n\ninterface NavigationChild {\n name: string;\n href: string;\n icon?: React.ComponentType>;\n tooltip?: string;\n}\n\ninterface NavigationItem {\n name: string;\n href?: string;\n icon: React.ComponentType>;\n children?: NavigationChild[];\n isLogout?: boolean;\n}\n\nconst baseNavigation: NavigationItem[] = [\n { name: \"Dashboard\", href: \"/dashboard\", icon: HomeIcon },\n { name: \"Orders\", href: \"/orders\", icon: ClipboardDocumentListIcon },\n {\n name: \"Billing\",\n icon: CreditCardIcon,\n children: [\n { name: \"Invoices\", href: \"/billing/invoices\" },\n { name: \"Payment Methods\", href: \"/billing/payments\" },\n ],\n },\n {\n name: \"Subscriptions\",\n icon: ServerIcon,\n children: [{ name: \"All Subscriptions\", href: \"/subscriptions\" }],\n },\n { name: \"Catalog\", href: \"/catalog\", icon: Squares2X2Icon },\n {\n name: \"Support\",\n icon: ChatBubbleLeftRightIcon,\n children: [\n { name: \"Cases\", href: \"/support/cases\" },\n { name: \"New Case\", href: \"/support/new\" },\n { name: \"Knowledge Base\", href: \"/support/kb\" },\n ],\n },\n {\n name: \"Account\",\n icon: UserIcon,\n children: [\n { name: \"Profile\", href: \"/account/profile\" },\n { name: \"Security\", href: \"/account/security\" },\n { name: \"Notifications\", href: \"/account/notifications\" },\n ],\n },\n { name: \"Log out\", href: \"#\", icon: ArrowRightStartOnRectangleIcon, isLogout: true },\n];\n\nexport function DashboardLayout({ children }: DashboardLayoutProps) {\n const [sidebarOpen, setSidebarOpen] = useState(false);\n const [mounted, setMounted] = useState(false);\n const { user, isAuthenticated, checkAuth } = useAuthStore();\n const pathname = usePathname();\n const router = useRouter();\n const { data: activeSubscriptions } = useActiveSubscriptions();\n\n // Initialize expanded items from localStorage\n const [expandedItems, setExpandedItems] = useState(() => {\n if (typeof window !== \"undefined\") {\n const saved = localStorage.getItem(\"sidebar-expanded-items\");\n if (saved) {\n try {\n const parsed = JSON.parse(saved) as unknown;\n if (Array.isArray(parsed) && parsed.every(x => typeof x === \"string\")) {\n return parsed;\n }\n } catch {\n // ignore\n }\n }\n }\n return [];\n });\n\n // Save expanded items to localStorage\n useEffect(() => {\n if (mounted) {\n localStorage.setItem(\"sidebar-expanded-items\", JSON.stringify(expandedItems));\n }\n }, [expandedItems, mounted]);\n\n useEffect(() => {\n setMounted(true);\n void checkAuth();\n }, [checkAuth]);\n\n useEffect(() => {\n if (mounted && !isAuthenticated) {\n router.push(\"/auth/login\");\n }\n }, [mounted, isAuthenticated, router]);\n\n // Auto-expand sections when browsing their routes\n useEffect(() => {\n const newExpanded: string[] = [];\n\n if (pathname.startsWith(\"/subscriptions\") && !expandedItems.includes(\"Subscriptions\")) {\n newExpanded.push(\"Subscriptions\");\n }\n if (pathname.startsWith(\"/billing\") && !expandedItems.includes(\"Billing\")) {\n newExpanded.push(\"Billing\");\n }\n if (pathname.startsWith(\"/support\") && !expandedItems.includes(\"Support\")) {\n newExpanded.push(\"Support\");\n }\n if (pathname.startsWith(\"/account\") && !expandedItems.includes(\"Account\")) {\n newExpanded.push(\"Account\");\n }\n\n if (newExpanded.length > 0) {\n setExpandedItems(prev => [...prev, ...newExpanded]);\n }\n }, [pathname, expandedItems]);\n\n const toggleExpanded = (itemName: string) => {\n setExpandedItems(prev =>\n prev.includes(itemName) ? prev.filter(name => name !== itemName) : [...prev, itemName]\n );\n };\n\n // Memoize navigation to prevent unnecessary re-renders\n const navigation = useMemo(() => computeNavigation(activeSubscriptions), [activeSubscriptions]);\n\n // Show loading state until mounted and auth is checked\n if (!mounted) {\n return (\n
\n
\n \n

Loading...

\n
\n
\n );\n }\n\n return (\n <>\n
\n {/* Mobile sidebar overlay */}\n {sidebarOpen && (\n
\n setSidebarOpen(false)}\n />\n
\n
\n setSidebarOpen(false)}\n >\n \n \n
\n \n
\n
\n )}\n\n {/* Desktop sidebar */}\n
\n
\n \n
\n
\n\n {/* Main content */}\n
\n {/* Header */}\n
setSidebarOpen(true)} user={user} />\n\n {/* Main content area */}\n
{children}
\n
\n
\n\n {/* Session timeout warning */}\n \n \n );\n}\n\nfunction computeNavigation(activeSubscriptions?: Subscription[]): NavigationItem[] {\n // Clone base structure\n const nav: NavigationItem[] = baseNavigation.map(item => ({\n ...item,\n children: item.children ? [...item.children] : undefined,\n }));\n\n // Inject dynamic submenu under Subscriptions\n const subIdx = nav.findIndex(n => n.name === \"Subscriptions\");\n if (subIdx >= 0) {\n const dynamicChildren: NavigationChild[] = (activeSubscriptions || []).map(sub => {\n const href = `/subscriptions/${sub.id}`;\n return {\n name: truncate(sub.productName || `Subscription ${sub.id}`, 28),\n href,\n tooltip: sub.productName || `Subscription ${sub.id}`,\n } as NavigationChild;\n });\n\n nav[subIdx] = {\n ...nav[subIdx],\n children: [{ name: \"All Subscriptions\", href: \"/subscriptions\" }, ...dynamicChildren],\n };\n }\n\n return nav;\n}\n\nfunction truncate(text: string, max: number): string {\n if (text.length <= max) return text;\n return text.slice(0, Math.max(0, max - 1)) + \"…\";\n}\n\ninterface SidebarProps {\n navigation: NavigationItem[];\n pathname: string;\n expandedItems: string[];\n toggleExpanded: (name: string) => void;\n isMobile?: boolean;\n}\n\nconst Sidebar = memo(function Sidebar({\n navigation,\n pathname,\n expandedItems,\n toggleExpanded,\n}: SidebarProps) {\n return (\n
\n {/* Logo Section */}\n
\n
\n
\n \n
\n
\n \n Assist Solutions\n \n

Customer Portal

\n
\n
\n
\n\n {/* Navigation */}\n
\n \n
\n
\n );\n});\n\ninterface HeaderProps {\n onMenuClick: () => void;\n user: any;\n}\n\nconst Header = memo(function Header({ onMenuClick, user }: HeaderProps) {\n return (\n
\n
\n {/* Mobile menu button */}\n \n \n \n\n {/* Spacer */}\n
\n\n {/* Global Utilities */}\n
\n \n \n \n \n\n \n \n \n\n \n {user?.firstName || user?.email?.split(\"@\")[0] || \"Account\"}\n \n
\n
\n
\n );\n});\n\nconst NavigationItem = memo(function NavigationItem({\n item,\n pathname,\n isExpanded,\n toggleExpanded,\n}: {\n item: NavigationItem;\n pathname: string;\n isExpanded: boolean;\n toggleExpanded: (name: string) => void;\n}) {\n const { logout } = useAuthStore();\n const router = useRouter();\n\n const hasChildren = item.children && item.children.length > 0;\n const isActive = hasChildren\n ? item.children?.some((child: NavigationChild) =>\n pathname.startsWith((child.href || \"\").split(/[?#]/)[0])\n ) || false\n : item.href\n ? pathname === item.href\n : false;\n\n const handleLogout = () => {\n void logout().then(() => {\n router.push(\"/\");\n });\n };\n\n if (hasChildren) {\n return (\n
\n toggleExpanded(item.name)}\n aria-expanded={isExpanded}\n className={`group w-full flex items-center px-3 py-2.5 text-left text-sm font-medium rounded-lg transition-all duration-200 relative ${\n isActive\n ? \"text-[var(--cp-sidebar-active-text)] bg-[var(--cp-sidebar-active-bg)]\"\n : \"text-[var(--cp-sidebar-text)] hover:text-[var(--cp-sidebar-text-hover)] hover:bg-[var(--cp-sidebar-hover-bg)]\"\n } focus:outline-none focus:ring-2 focus:ring-primary/20`}\n >\n {/* Active indicator */}\n {isActive && (\n
\n )}\n\n \n \n
\n\n {item.name}\n\n \n \n \n \n\n {/* Animated dropdown */}\n \n
\n {item.children?.map((child: NavigationChild) => {\n const isChildActive = pathname === (child.href || \"\").split(/[?#]/)[0];\n return (\n \n {/* Child active indicator */}\n {isChildActive && (\n
\n )}\n\n \n\n {child.name}\n \n );\n })}\n
\n
\n
\n );\n }\n\n if (item.isLogout) {\n return (\n \n
\n \n
\n {item.name}\n \n );\n }\n\n return (\n \n {/* Active indicator */}\n {isActive && (\n
\n )}\n\n \n \n
\n\n {item.name}\n \n );\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/DashboardLayout/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/PageLayout/PageLayout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/PageLayout/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/layout/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/lazy/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/animated-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/button.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'_as' is assigned a value but never used.","line":84,"column":17,"nodeType":null,"messageId":"unusedVar","endLine":84,"endColumn":20},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'_as' is assigned a value but never used.","line":97,"column":15,"nodeType":null,"messageId":"unusedVar","endLine":97,"endColumn":18}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import type { AnchorHTMLAttributes, ButtonHTMLAttributes } from \"react\";\nimport { forwardRef } from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/utils\";\nimport { ArrowPathIcon } from \"@heroicons/react/24/outline\";\n\nconst buttonVariants = cva(\n \"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background\",\n {\n variants: {\n variant: {\n default: \"bg-blue-600 text-white hover:bg-blue-700\",\n destructive: \"bg-red-600 text-white hover:bg-red-700\",\n outline: \"border border-gray-300 bg-white hover:bg-gray-50 text-gray-900\",\n secondary: \"bg-gray-100 text-gray-900 hover:bg-gray-200\",\n ghost: \"hover:bg-gray-100 text-gray-900\",\n link: \"underline-offset-4 hover:underline text-blue-600\",\n },\n size: {\n xs: \"h-8 px-2 text-xs\",\n sm: \"h-9 px-3 text-sm\",\n default: \"h-10 py-2 px-4\",\n lg: \"h-11 px-8 text-base\",\n xl: \"h-12 px-10 text-lg\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\ninterface BaseButtonProps extends VariantProps {\n loading?: boolean;\n loadingText?: string;\n leftIcon?: React.ReactNode;\n rightIcon?: React.ReactNode;\n disabled?: boolean;\n}\n\ntype ButtonAsAnchorProps = {\n as: \"a\";\n href: string;\n} & AnchorHTMLAttributes &\n BaseButtonProps;\n\ntype ButtonAsButtonProps = {\n as?: \"button\";\n} & ButtonHTMLAttributes &\n BaseButtonProps;\n\nexport type ButtonProps = ButtonAsAnchorProps | ButtonAsButtonProps;\n\nconst Button = forwardRef((props, ref) => {\n const {\n className,\n variant,\n size,\n loading = false,\n loadingText,\n leftIcon,\n rightIcon,\n children,\n disabled,\n ...restProps\n } = props;\n\n const isDisabled = disabled || loading;\n\n const content = (\n <>\n {loading ? (\n \n ) : (\n leftIcon && {leftIcon}\n )}\n {loading && loadingText ? loadingText : children}\n {!loading && rightIcon && {rightIcon}}\n \n );\n\n if (props.as === \"a\") {\n const { as: _as, href, ...anchorProps } = restProps as ButtonAsAnchorProps;\n return (\n }\n {...anchorProps}\n >\n {content}\n \n );\n }\n\n const { as: _as, ...buttonProps } = restProps as ButtonAsButtonProps;\n return (\n }\n {...buttonProps}\n >\n {content}\n \n );\n});\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/empty-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/error-message.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/error-state.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/inline-toast.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/input.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/label.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/loading-skeleton.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'title' is defined but never used.","line":95,"column":36,"nodeType":null,"messageId":"unusedVar","endLine":95,"endColumn":41}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { cn } from \"@/lib/utils\";\n\ninterface SkeletonProps {\n className?: string;\n animate?: boolean;\n}\n\nexport function Skeleton({ className, animate = true }: SkeletonProps) {\n return (\n \n );\n}\n\nexport function LoadingCard({ className }: { className?: string }) {\n return (\n \n
\n
\n \n
\n \n \n
\n
\n
\n \n \n \n
\n
\n
\n );\n}\n\nexport function LoadingTable({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) {\n return (\n
\n {/* Header */}\n
\n
\n {Array.from({ length: columns }).map((_, i) => (\n \n ))}\n
\n
\n\n {/* Rows */}\n
\n {Array.from({ length: rows }).map((_, rowIndex) => (\n
\n
\n {Array.from({ length: columns }).map((_, colIndex) => (\n \n ))}\n
\n
\n ))}\n
\n
\n );\n}\n\nexport function LoadingStats({ count = 4 }: { count?: number }) {\n return (\n
\n {Array.from({ length: count }).map((_, i) => (\n \n
\n \n
\n \n \n
\n
\n
\n ))}\n \n );\n}\n\nexport function PageLoadingState({ title }: { title: string }) {\n return (\n
\n
\n {/* Header skeleton */}\n
\n
\n \n
\n \n \n
\n
\n
\n\n {/* Content skeleton */}\n
\n \n \n
\n
\n
\n );\n}\n\nexport function FullPageLoadingState({ title }: { title: string }) {\n return (\n
\n
\n
\n
\n

{title}

\n

Please wait while we load your content...

\n
\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/loading-spinner.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/logo.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/progress-steps.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/status-pill.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/step-header.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/components/ui/sub-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/components/AddressCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/components/PasswordChangeCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/components/PersonalInfoCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/containers/Profile.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'loading' is assigned a value but never used.","line":14,"column":5,"nodeType":null,"messageId":"unusedVar","endLine":14,"endColumn":12},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'billingInfo' is assigned a value but never used.","line":16,"column":5,"nodeType":null,"messageId":"unusedVar","endLine":16,"endColumn":16},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":76,"column":18,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":79,"endColumn":13},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":83,"column":20,"nodeType":"TSAsExpression","messageId":"anyAssignment","endLine":83,"endColumn":38},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":83,"column":35,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":83,"endColumn":38,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2757,2760],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2757,2760],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":89,"column":18,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":92,"endColumn":13},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `AddressData`.","line":90,"column":42,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":90,"endColumn":60},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":90,"column":57,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":90,"endColumn":60,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3055,3058],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3055,3058],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `SetStateAction`.","line":93,"column":51,"nodeType":"TSAsExpression","messageId":"unsafeArgument","endLine":93,"endColumn":62},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":93,"column":59,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":93,"endColumn":62,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3180,3183],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3180,3183],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":8,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useState } from \"react\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { useProfileData } from \"../hooks/useProfileData\";\nimport { PersonalInfoCard } from \"../components/PersonalInfoCard\";\nimport { AddressCard } from \"../components/AddressCard\";\nimport { PasswordChangeCard } from \"../components/PasswordChangeCard\";\n\nexport function ProfileContainer() {\n const { user } = useAuthStore();\n const {\n loading,\n error,\n billingInfo,\n formData,\n setFormData,\n addressData,\n setAddressData,\n saveProfile,\n saveAddress,\n isSavingProfile,\n isSavingAddress,\n } = useProfileData();\n\n const [isEditingInfo, setIsEditingInfo] = useState(false);\n const [isEditingAddress, setIsEditingAddress] = useState(false);\n\n const [pwdError, setPwdError] = useState(null);\n const [pwdSuccess, setPwdSuccess] = useState(null);\n const [isChangingPassword, setIsChangingPassword] = useState(false);\n const [pwdForm, setPwdForm] = useState({\n currentPassword: \"\",\n newPassword: \"\",\n confirmPassword: \"\",\n });\n\n const handleChangePassword = async () => {\n setIsChangingPassword(true);\n setPwdError(null);\n setPwdSuccess(null);\n try {\n if (!pwdForm.currentPassword || !pwdForm.newPassword) {\n setPwdError(\"Please fill in all password fields\");\n return;\n }\n if (pwdForm.newPassword !== pwdForm.confirmPassword) {\n setPwdError(\"New password and confirmation do not match\");\n return;\n }\n await useAuthStore.getState().changePassword(pwdForm.currentPassword, pwdForm.newPassword);\n setPwdSuccess(\"Password changed successfully.\");\n setPwdForm({ currentPassword: \"\", newPassword: \"\", confirmPassword: \"\" });\n } catch (err) {\n setPwdError(err instanceof Error ? err.message : \"Failed to change password\");\n } finally {\n setIsChangingPassword(false);\n }\n };\n\n return (\n }\n >\n
\n setIsEditingInfo(true)}\n onCancel={() => setIsEditingInfo(false)}\n onChange={(field, value) => setFormData(prev => ({ ...prev, [field]: value }))}\n onSave={async () => {\n const ok = await saveProfile(formData);\n if (ok) setIsEditingInfo(false);\n }}\n />\n\n setIsEditingAddress(true)}\n onCancel={() => setIsEditingAddress(false)}\n onSave={async () => {\n const ok = await saveAddress(addressData as any);\n if (ok) setIsEditingAddress(false);\n }}\n onAddressChange={addr => setAddressData(addr as any)}\n />\n\n setPwdForm(prev => ({ ...prev, ...next }))}\n onSubmit={() => {\n void handleChangePassword();\n }}\n />\n
\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/containers/ProfileContainer.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'PageLayout' is defined but never used.","line":4,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":4,"endColumn":20},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has a missing dependency: 'address'. Either include it or remove the dependency array.","line":63,"column":6,"nodeType":"ArrayExpression","endLine":63,"endColumn":8,"suggestions":[{"desc":"Update the dependencies array to be: [address]","fix":{"range":[1932,1934],"text":"[address]"}}]},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":187,"column":27,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":190,"endColumn":21},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":265,"column":29,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":268,"endColumn":23}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport {\n ExclamationTriangleIcon,\n MapPinIcon,\n PencilIcon,\n CheckIcon,\n XMarkIcon,\n UserIcon,\n} from \"@heroicons/react/24/outline\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { accountService } from \"@/features/account/services/account.service\";\nimport { useProfileEdit } from \"@/features/account/hooks/useProfileEdit\";\nimport { AddressForm } from \"@/features/catalog/components/base/AddressForm\";\nimport { useAddressEdit } from \"@/features/account/hooks/useAddressEdit\";\n\nexport default function ProfileContainer() {\n const { user } = useAuthStore();\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(null);\n const [editingProfile, setEditingProfile] = useState(false);\n const [editingAddress, setEditingAddress] = useState(false);\n\n const profile = useProfileEdit({\n firstName: user?.firstName || \"\",\n lastName: user?.lastName || \"\",\n phone: user?.phone || \"\",\n });\n\n const address = useAddressEdit({\n street: \"\",\n streetLine2: \"\",\n city: \"\",\n state: \"\",\n postalCode: \"\",\n country: \"\",\n });\n\n useEffect(() => {\n void (async () => {\n try {\n setLoading(true);\n const addr = await accountService.getAddress().catch(() => null);\n if (addr) {\n address.setForm({\n street: addr.street ?? \"\",\n streetLine2: addr.streetLine2 ?? \"\",\n city: addr.city ?? \"\",\n state: addr.state ?? \"\",\n postalCode: addr.postalCode ?? \"\",\n country: addr.country ?? \"\",\n });\n }\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to load profile data\");\n } finally {\n setLoading(false);\n }\n })();\n }, []);\n\n if (loading) {\n return (\n
\n
\n
\n \n Loading profile...\n
\n
\n
\n );\n }\n\n return (\n
\n
\n {error && (\n
\n
\n \n
\n

Error

\n

{error}

\n
\n
\n
\n )}\n\n {/* Personal Information */}\n
\n
\n
\n
\n \n

Personal Information

\n
\n {!editingProfile && (\n setEditingProfile(true)}\n className=\"inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 transition-colors\"\n >\n \n Edit\n \n )}\n
\n
\n\n
\n
\n
\n \n {editingProfile ? (\n profile.setField(\"firstName\", e.target.value)}\n className=\"block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors\"\n />\n ) : (\n

\n {user?.firstName || Not provided}\n

\n )}\n
\n
\n \n {editingProfile ? (\n profile.setField(\"lastName\", e.target.value)}\n className=\"block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors\"\n />\n ) : (\n

\n {user?.lastName || Not provided}\n

\n )}\n
\n
\n \n
\n
\n

{user?.email}

\n
\n

\n Email cannot be changed from the portal.\n

\n
\n
\n
\n \n {editingProfile ? (\n profile.setField(\"phone\", e.target.value)}\n placeholder=\"+81 XX-XXXX-XXXX\"\n className=\"block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors\"\n />\n ) : (\n

\n {user?.phone || Not provided}\n

\n )}\n
\n
\n\n {editingProfile && (\n
\n setEditingProfile(false)}\n disabled={profile.saving}\n className=\"inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50\"\n >\n \n Cancel\n \n {\n const ok = await profile.save();\n if (ok) setEditingProfile(false);\n }}\n disabled={profile.saving}\n className=\"inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50\"\n >\n {profile.saving ? (\n <>\n
\n Saving...\n \n ) : (\n <>\n \n Save Changes\n \n )}\n \n
\n )}\n
\n
\n\n {/* Address */}\n
\n
\n
\n
\n \n

Address Information

\n
\n {!editingAddress && (\n setEditingAddress(true)}\n className=\"inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 transition-colors\"\n >\n \n Edit\n \n )}\n
\n
\n\n
\n {editingAddress ? (\n
\n \n address.setForm({\n street: a.street,\n streetLine2: a.streetLine2,\n city: a.city,\n state: a.state,\n postalCode: a.postalCode,\n country: a.country,\n })\n }\n title=\"Mailing Address\"\n />\n
\n setEditingAddress(false)}\n disabled={address.saving}\n className=\"inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-lg text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 transition-colors\"\n >\n \n Cancel\n \n {\n const ok = await address.save();\n if (ok) setEditingAddress(false);\n }}\n disabled={address.saving}\n className=\"inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 transition-colors\"\n >\n {address.saving ? (\n <>\n
\n Saving...\n \n ) : (\n <>\n \n Save Address\n \n )}\n \n
\n {address.error && (\n
\n
\n \n
\n

Address Error

\n

{address.error}

\n
\n
\n
\n )}\n
\n ) : (\n
\n {address.form.street || address.form.city ? (\n
\n
\n {address.form.street &&

{address.form.street}

}\n {address.form.streetLine2 &&

{address.form.streetLine2}

}\n

\n {[address.form.city, address.form.state, address.form.postalCode]\n .filter(Boolean)\n .join(\", \")}\n

\n

{address.form.country}

\n
\n
\n ) : (\n
\n \n

No address on file

\n setEditingAddress(true)}\n className=\"bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors\"\n >\n Add Address\n \n
\n )}\n
\n )}\n
\n
\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/hooks/useAddressEdit.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/hooks/useAddressForm.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/hooks/useProfileData.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/hooks/useProfileEdit.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/account/services/account.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":67,"column":77,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":67,"endColumn":80,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1678,1681],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1678,1681],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":71,"column":34,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":71,"endColumn":39},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useCallback has a missing dependency: 'validationConfig'. Either include it or remove the dependency array.","line":73,"column":6,"nodeType":"ArrayExpression","endLine":73,"endColumn":8,"suggestions":[{"desc":"Update the dependencies array to be: [validationConfig]","fix":{"range":[1900,1902],"text":"[validationConfig]"}}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":77,"column":41,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":77,"endColumn":44,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2012,2015],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2012,2015],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":78,"column":39,"nodeType":"Property","messageId":"anyAssignment","endLine":78,"endColumn":53},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":165,"column":20,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":165,"endColumn":34},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":240,"column":46,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6828,6851],"text":"Don't have an account? "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6828,6851],"text":"Don‘t have an account? "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6828,6851],"text":"Don't have an account? "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6828,6851],"text":"Don’t have an account? "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":6,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Login Form Component\n * Reusable login form with validation and error handling\n */\n\n\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport Link from \"next/link\";\nimport { Button, Input, ErrorMessage } from \"@/components/ui\";\nimport { FormField } from \"@/components/common/FormField\";\nimport { useLogin } from \"../../hooks/use-auth\";\nimport { validationRules, validateField } from \"@/lib/form-validation\";\n\ninterface LoginFormProps {\n onSuccess?: () => void;\n onError?: (error: string) => void;\n showSignupLink?: boolean;\n showForgotPasswordLink?: boolean;\n className?: string;\n}\n\ninterface LoginFormData {\n email: string;\n password: string;\n rememberMe: boolean;\n}\n\ninterface FormErrors {\n email?: string;\n password?: string;\n general?: string;\n}\n\nexport function LoginForm({\n onSuccess,\n onError,\n showSignupLink = true,\n showForgotPasswordLink = true,\n className = \"\",\n}: LoginFormProps) {\n const { login, loading, error, clearError } = useLogin();\n\n const [formData, setFormData] = useState({\n email: \"\",\n password: \"\",\n rememberMe: false,\n });\n\n const [errors, setErrors] = useState({});\n const [touched, setTouched] = useState>({\n email: false,\n password: false,\n rememberMe: false,\n });\n\n // Validation rules\n const validationConfig = {\n email: [\n validationRules.required(\"Email is required\"),\n validationRules.email(\"Please enter a valid email address\"),\n ],\n password: [validationRules.required(\"Password is required\")],\n };\n\n // Validate field\n const validateFormField = useCallback((field: keyof LoginFormData, value: any) => {\n const rules = validationConfig[field as keyof typeof validationConfig];\n if (!rules) return null;\n\n const result = validateField(value, rules);\n return result.isValid ? null : result.errors[0];\n }, []);\n\n // Handle field change\n const handleFieldChange = useCallback(\n (field: keyof LoginFormData, value: any) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n\n // Clear general error when user starts typing\n if (errors.general) {\n setErrors(prev => ({ ...prev, general: undefined }));\n clearError();\n }\n\n // Validate field if it has been touched\n if (touched[field]) {\n const fieldError = validateFormField(field, value);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n }\n },\n [errors.general, touched, validateFormField, clearError]\n );\n\n // Handle field blur\n const handleFieldBlur = useCallback(\n (field: keyof LoginFormData) => {\n setTouched(prev => ({ ...prev, [field]: true }));\n\n const fieldError = validateFormField(field, formData[field]);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n },\n [formData, validateFormField]\n );\n\n // Validate entire form\n const validateForm = useCallback(() => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n // Validate email\n const emailError = validateFormField(\"email\", formData.email);\n if (emailError) {\n newErrors.email = emailError;\n isValid = false;\n }\n\n // Validate password\n const passwordError = validateFormField(\"password\", formData.password);\n if (passwordError) {\n newErrors.password = passwordError;\n isValid = false;\n }\n\n setErrors(newErrors);\n return isValid;\n }, [formData, validateFormField]);\n\n // Handle form submission\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n setTouched({\n email: true,\n password: true,\n rememberMe: true,\n });\n\n // Validate form\n if (!validateForm()) {\n return;\n }\n\n try {\n await login({\n email: formData.email.trim(),\n password: formData.password,\n });\n onSuccess?.();\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Login failed\";\n setErrors(prev => ({ ...prev, general: errorMessage }));\n onError?.(errorMessage);\n }\n },\n [formData, validateForm, login, onSuccess, onError]\n );\n\n // Check if form is valid\n const isFormValidState = !errors.email && !errors.password && formData.email && formData.password;\n\n return (\n
\n {/* General Error */}\n {(errors.general || error) && (\n \n {errors.general || error}\n \n )}\n\n {/* Email Field */}\n \n handleFieldChange(\"email\", e.target.value)}\n onBlur={() => handleFieldBlur(\"email\")}\n placeholder=\"Enter your email address\"\n disabled={loading}\n error={errors.email}\n autoComplete=\"email\"\n autoFocus\n />\n \n\n {/* Password Field */}\n \n handleFieldChange(\"password\", e.target.value)}\n onBlur={() => handleFieldBlur(\"password\")}\n placeholder=\"Enter your password\"\n disabled={loading}\n error={errors.password}\n autoComplete=\"current-password\"\n />\n \n\n {/* Remember Me */}\n
\n \n\n {showForgotPasswordLink && (\n \n Forgot password?\n \n )}\n
\n\n {/* Submit Button */}\n \n {loading ? \"Signing in...\" : \"Sign In\"}\n \n\n {/* Signup Link */}\n {showSignupLink && (\n
\n Don't have an account? \n \n Sign up\n \n
\n )}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/LoginForm/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ForgotPasswordRequest' is defined but never used.","line":14,"column":15,"nodeType":null,"messageId":"unusedVar","endLine":14,"endColumn":36},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ResetPasswordRequest' is defined but never used.","line":14,"column":38,"nodeType":null,"messageId":"unusedVar","endLine":14,"endColumn":58},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":83,"column":28,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":83,"endColumn":31,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2287,2290],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2287,2290],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":94,"column":36,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":94,"endColumn":41},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useCallback has a missing dependency: 'validationConfig'. Either include it or remove the dependency array.","line":97,"column":5,"nodeType":"ArrayExpression","endLine":97,"endColumn":25,"suggestions":[{"desc":"Update the dependencies array to be: [resetData.password, validationConfig]","fix":{"range":[2777,2797],"text":"[resetData.password, validationConfig]"}}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":102,"column":28,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":102,"endColumn":31,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2897,2900],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2897,2900],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":104,"column":44,"nodeType":"Property","messageId":"anyAssignment","endLine":104,"endColumn":58},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":106,"column":42,"nodeType":"Property","messageId":"anyAssignment","endLine":106,"endColumn":56},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":252,"column":15,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[7393,7442],"text":"\n We've sent a password reset link to "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[7393,7442],"text":"\n We‘ve sent a password reset link to "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[7393,7442],"text":"\n We've sent a password reset link to "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[7393,7442],"text":"\n We’ve sent a password reset link to "},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":258,"column":17,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[7593,7679],"text":"\n Didn't receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[7593,7679],"text":"\n Didn‘t receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[7593,7679],"text":"\n Didn't receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[7593,7679],"text":"\n Didn’t receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `’`."}]},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":289,"column":20,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":289,"endColumn":34},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":303,"column":46,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we'll send you a link to reset your password.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we‘ll send you a link to reset your password.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we'll send you a link to reset your password.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we’ll send you a link to reset your password.\n "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":9,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Password Reset Form Component\n * Form for requesting and resetting passwords\n */\n\n\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport Link from \"next/link\";\nimport { Button, Input, ErrorMessage } from \"@/components/ui\";\nimport { FormField } from \"@/components/common/FormField\";\nimport { usePasswordReset } from \"../../hooks/use-auth\";\nimport { validationRules, validateField } from \"@/lib/form-validation\";\nimport type { ForgotPasswordRequest, ResetPasswordRequest } from \"@/lib/types\";\n\ninterface PasswordResetFormProps {\n mode: \"request\" | \"reset\";\n token?: string;\n onSuccess?: () => void;\n onError?: (error: string) => void;\n showLoginLink?: boolean;\n className?: string;\n}\n\ninterface RequestFormData {\n email: string;\n}\n\ninterface ResetFormData {\n password: string;\n confirmPassword: string;\n}\n\ninterface FormErrors {\n email?: string;\n password?: string;\n confirmPassword?: string;\n general?: string;\n}\n\nexport function PasswordResetForm({\n mode,\n token,\n onSuccess,\n onError,\n showLoginLink = true,\n className = \"\",\n}: PasswordResetFormProps) {\n const { requestPasswordReset, resetPassword, loading, error, clearError } = usePasswordReset();\n\n const [requestData, setRequestData] = useState({\n email: \"\",\n });\n\n const [resetData, setResetData] = useState({\n password: \"\",\n confirmPassword: \"\",\n });\n\n const [errors, setErrors] = useState({});\n const [touched, setTouched] = useState>({});\n const [isSubmitted, setIsSubmitted] = useState(false);\n\n // Validation rules\n const validationConfig = {\n email: [\n validationRules.required(\"Email is required\"),\n validationRules.email(\"Please enter a valid email address\"),\n ],\n password: [\n validationRules.required(\"Password is required\"),\n validationRules.minLength(8, \"Password must be at least 8 characters\"),\n validationRules.pattern(\n /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/,\n \"Password must contain at least one uppercase letter, one lowercase letter, and one number\"\n ),\n ],\n confirmPassword: [validationRules.required(\"Please confirm your password\")],\n };\n\n // Validate field\n const validateFormField = useCallback(\n (field: string, value: any) => {\n const rules = validationConfig[field as keyof typeof validationConfig];\n if (!rules) return null;\n\n // Special validation for confirm password\n if (field === \"confirmPassword\") {\n if (!value) return \"Please confirm your password\";\n if (value !== resetData.password) return \"Passwords do not match\";\n return null;\n }\n\n const result = validateField(value, rules);\n return result.isValid ? null : result.errors[0];\n },\n [resetData.password]\n );\n\n // Handle field change\n const handleFieldChange = useCallback(\n (field: string, value: any) => {\n if (mode === \"request\") {\n setRequestData(prev => ({ ...prev, [field]: value }));\n } else {\n setResetData(prev => ({ ...prev, [field]: value }));\n }\n\n // Clear general error when user starts typing\n if (errors.general) {\n setErrors(prev => ({ ...prev, general: undefined }));\n clearError();\n }\n\n // Validate field if it has been touched\n if (touched[field]) {\n const fieldError = validateFormField(field, value);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n }\n\n // Also validate confirm password when password changes\n if (field === \"password\" && touched.confirmPassword && mode === \"reset\") {\n const confirmPasswordError = validateFormField(\n \"confirmPassword\",\n resetData.confirmPassword\n );\n setErrors(prev => ({ ...prev, confirmPassword: confirmPasswordError || undefined }));\n }\n },\n [mode, errors.general, touched, validateFormField, clearError, resetData.confirmPassword]\n );\n\n // Handle field blur\n const handleFieldBlur = useCallback(\n (field: string) => {\n setTouched(prev => ({ ...prev, [field]: true }));\n\n const value =\n mode === \"request\"\n ? requestData[field as keyof RequestFormData]\n : resetData[field as keyof ResetFormData];\n const fieldError = validateFormField(field, value);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n },\n [mode, requestData, resetData, validateFormField]\n );\n\n // Validate form\n const validateForm = useCallback(() => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n if (mode === \"request\") {\n const emailError = validateFormField(\"email\", requestData.email);\n if (emailError) {\n newErrors.email = emailError;\n isValid = false;\n }\n } else {\n const passwordError = validateFormField(\"password\", resetData.password);\n if (passwordError) {\n newErrors.password = passwordError;\n isValid = false;\n }\n\n const confirmPasswordError = validateFormField(\"confirmPassword\", resetData.confirmPassword);\n if (confirmPasswordError) {\n newErrors.confirmPassword = confirmPasswordError;\n isValid = false;\n }\n }\n\n setErrors(newErrors);\n return isValid;\n }, [mode, requestData, resetData, validateFormField]);\n\n // Handle form submission\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n if (mode === \"request\") {\n setTouched({ email: true });\n } else {\n setTouched({ password: true, confirmPassword: true });\n }\n\n // Validate form\n if (!validateForm()) {\n return;\n }\n\n try {\n if (mode === \"request\") {\n await requestPasswordReset({ email: requestData.email.trim() });\n setIsSubmitted(true);\n } else {\n if (!token) {\n throw new Error(\"Reset token is required\");\n }\n\n await resetPassword({\n token,\n password: resetData.password,\n confirmPassword: resetData.confirmPassword,\n });\n }\n\n onSuccess?.();\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Operation failed\";\n setErrors(prev => ({ ...prev, general: errorMessage }));\n onError?.(errorMessage);\n }\n },\n [\n mode,\n token,\n requestData,\n resetData,\n validateForm,\n requestPasswordReset,\n resetPassword,\n onSuccess,\n onError,\n ]\n );\n\n // Show success message for request mode\n if (mode === \"request\" && isSubmitted) {\n return (\n
\n
\n
\n \n \n \n
\n

Check your email

\n

\n We've sent a password reset link to {requestData.email}\n

\n
\n\n
\n

\n Didn't receive the email? Check your spam folder or try again.\n

\n\n {\n setIsSubmitted(false);\n setErrors({});\n setTouched({});\n }}\n className=\"w-full\"\n >\n Try Again\n \n
\n\n {showLoginLink && (\n
\n \n Back to Sign In\n \n
\n )}\n
\n );\n }\n\n return (\n
\n {/* General Error */}\n {(errors.general || error) && (\n \n {errors.general || error}\n \n )}\n\n {mode === \"request\" ? (\n <>\n {/* Request Mode - Email Input */}\n
\n

Reset your password

\n

\n Enter your email address and we'll send you a link to reset your password.\n

\n
\n\n \n handleFieldChange(\"email\", e.target.value)}\n onBlur={() => handleFieldBlur(\"email\")}\n placeholder=\"Enter your email address\"\n disabled={loading}\n error={errors.email}\n autoComplete=\"email\"\n autoFocus\n />\n \n\n \n {loading ? \"Sending...\" : \"Send Reset Link\"}\n \n \n ) : (\n <>\n {/* Reset Mode - Password Inputs */}\n
\n

Set new password

\n

Enter your new password below.

\n
\n\n \n handleFieldChange(\"password\", e.target.value)}\n onBlur={() => handleFieldBlur(\"password\")}\n placeholder=\"Enter your new password\"\n disabled={loading}\n error={errors.password}\n autoComplete=\"new-password\"\n autoFocus\n />\n \n\n \n handleFieldChange(\"confirmPassword\", e.target.value)}\n onBlur={() => handleFieldBlur(\"confirmPassword\")}\n placeholder=\"Confirm your new password\"\n disabled={loading}\n error={errors.confirmPassword}\n autoComplete=\"new-password\"\n />\n \n\n \n {loading ? \"Updating...\" : \"Update Password\"}\n \n \n )}\n\n {/* Login Link */}\n {showLoginLink && (\n
\n \n Back to Sign In\n \n
\n )}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/PasswordResetForm/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":77,"column":36,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":77,"endColumn":39,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2080,2083],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2080,2083],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":88,"column":36,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":88,"endColumn":41},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useCallback has a missing dependency: 'validationConfig'. Either include it or remove the dependency array.","line":91,"column":5,"nodeType":"ArrayExpression","endLine":91,"endColumn":24,"suggestions":[{"desc":"Update the dependencies array to be: [formData.password, validationConfig]","fix":{"range":[2569,2588],"text":"[formData.password, validationConfig]"}}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":96,"column":36,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":96,"endColumn":39,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2696,2699],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2696,2699],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":97,"column":39,"nodeType":"Property","messageId":"anyAssignment","endLine":97,"endColumn":53},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":207,"column":22,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":207,"endColumn":36}],"suppressedMessages":[],"errorCount":5,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Set Password Form Component\n * Form for setting password after WHMCS account linking\n */\n\n\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport Link from \"next/link\";\nimport { Button, Input, ErrorMessage } from \"@/components/ui\";\nimport { FormField } from \"@/components/common/FormField\";\nimport { useWhmcsLink } from \"../../hooks/use-auth\";\nimport { validationRules, validateField } from \"@/lib/form-validation\";\n\ninterface SetPasswordFormProps {\n email?: string;\n onSuccess?: () => void;\n onError?: (error: string) => void;\n showLoginLink?: boolean;\n className?: string;\n}\n\ninterface FormData {\n email: string;\n password: string;\n confirmPassword: string;\n}\n\ninterface FormErrors {\n email?: string;\n password?: string;\n confirmPassword?: string;\n general?: string;\n}\n\nexport function SetPasswordForm({\n email: initialEmail = \"\",\n onSuccess,\n onError,\n showLoginLink = true,\n className = \"\",\n}: SetPasswordFormProps) {\n const { setPassword, loading, error, clearError } = useWhmcsLink();\n\n const [formData, setFormData] = useState({\n email: initialEmail,\n password: \"\",\n confirmPassword: \"\",\n });\n\n const [errors, setErrors] = useState({});\n const [touched, setTouched] = useState>({\n email: false,\n password: false,\n confirmPassword: false,\n });\n\n // Validation rules\n const validationConfig = {\n email: [\n validationRules.required(\"Email is required\"),\n validationRules.email(\"Please enter a valid email address\"),\n ],\n password: [\n validationRules.required(\"Password is required\"),\n validationRules.minLength(8, \"Password must be at least 8 characters\"),\n validationRules.pattern(\n /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/,\n \"Password must contain at least one uppercase letter, one lowercase letter, and one number\"\n ),\n ],\n confirmPassword: [validationRules.required(\"Please confirm your password\")],\n };\n\n // Validate field\n const validateFormField = useCallback(\n (field: keyof FormData, value: any) => {\n const rules = validationConfig[field as keyof typeof validationConfig];\n if (!rules) return null;\n\n // Special validation for confirm password\n if (field === \"confirmPassword\") {\n if (!value) return \"Please confirm your password\";\n if (value !== formData.password) return \"Passwords do not match\";\n return null;\n }\n\n const result = validateField(value, rules);\n return result.isValid ? null : result.errors[0];\n },\n [formData.password]\n );\n\n // Handle field change\n const handleFieldChange = useCallback(\n (field: keyof FormData, value: any) => {\n setFormData(prev => ({ ...prev, [field]: value }));\n\n // Clear general error when user starts typing\n if (errors.general) {\n setErrors(prev => ({ ...prev, general: undefined }));\n clearError();\n }\n\n // Validate field if it has been touched\n if (touched[field]) {\n const fieldError = validateFormField(field, value);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n }\n\n // Also validate confirm password when password changes\n if (field === \"password\" && touched.confirmPassword) {\n const confirmPasswordError = validateFormField(\"confirmPassword\", formData.confirmPassword);\n setErrors(prev => ({ ...prev, confirmPassword: confirmPasswordError || undefined }));\n }\n },\n [errors.general, touched, validateFormField, clearError, formData.confirmPassword]\n );\n\n // Handle field blur\n const handleFieldBlur = useCallback(\n (field: keyof FormData) => {\n setTouched(prev => ({ ...prev, [field]: true }));\n\n const fieldError = validateFormField(field, formData[field]);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n },\n [formData, validateFormField]\n );\n\n // Validate entire form\n const validateForm = useCallback(() => {\n const newErrors: FormErrors = {};\n let isValid = true;\n\n // Validate email\n const emailError = validateFormField(\"email\", formData.email);\n if (emailError) {\n newErrors.email = emailError;\n isValid = false;\n }\n\n // Validate password\n const passwordError = validateFormField(\"password\", formData.password);\n if (passwordError) {\n newErrors.password = passwordError;\n isValid = false;\n }\n\n // Validate confirm password\n const confirmPasswordError = validateFormField(\"confirmPassword\", formData.confirmPassword);\n if (confirmPasswordError) {\n newErrors.confirmPassword = confirmPasswordError;\n isValid = false;\n }\n\n setErrors(newErrors);\n return isValid;\n }, [formData, validateFormField]);\n\n // Handle form submission\n const handleSubmit = useCallback(\n async (e: React.FormEvent) => {\n e.preventDefault();\n\n // Mark all fields as touched\n setTouched({\n email: true,\n password: true,\n confirmPassword: true,\n });\n\n // Validate form\n if (!validateForm()) {\n return;\n }\n\n try {\n await setPassword(formData.email.trim(), formData.password);\n onSuccess?.();\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Failed to set password\";\n setErrors(prev => ({ ...prev, general: errorMessage }));\n onError?.(errorMessage);\n }\n },\n [formData, validateForm, setPassword, onSuccess, onError]\n );\n\n // Check if form is valid\n const isFormValid =\n !errors.email &&\n !errors.password &&\n !errors.confirmPassword &&\n formData.email &&\n formData.password &&\n formData.confirmPassword;\n\n return (\n
\n {/* Header */}\n
\n

Set Your Password

\n

Complete your account setup by creating a secure password.

\n
\n\n
\n {/* General Error */}\n {(errors.general || error) && (\n \n {errors.general || error}\n \n )}\n\n {/* Email Field */}\n \n handleFieldChange(\"email\", e.target.value)}\n onBlur={() => handleFieldBlur(\"email\")}\n placeholder=\"Enter your email address\"\n disabled={loading || !!initialEmail}\n error={errors.email}\n autoComplete=\"email\"\n autoFocus={!initialEmail}\n />\n {initialEmail && (\n

This email is linked to your WHMCS account

\n )}\n
\n\n {/* Password Field */}\n \n handleFieldChange(\"password\", e.target.value)}\n onBlur={() => handleFieldBlur(\"password\")}\n placeholder=\"Create a secure password\"\n disabled={loading}\n error={errors.password}\n autoComplete=\"new-password\"\n autoFocus={!!initialEmail}\n />\n
\n

Password requirements:

\n
    \n
  • At least 8 characters long
  • \n
  • Contains uppercase and lowercase letters
  • \n
  • Contains at least one number
  • \n
\n
\n
\n\n {/* Confirm Password Field */}\n \n handleFieldChange(\"confirmPassword\", e.target.value)}\n onBlur={() => handleFieldBlur(\"confirmPassword\")}\n placeholder=\"Confirm your password\"\n disabled={loading}\n error={errors.confirmPassword}\n autoComplete=\"new-password\"\n />\n \n\n {/* Submit Button */}\n \n {loading ? \"Setting Password...\" : \"Set Password & Continue\"}\n \n \n\n {/* Login Link */}\n {showLoginLink && (\n
\n Already have a password? \n \n Sign in\n \n
\n )}\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SetPasswordForm/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/AccountStep.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/AddressStep.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/PersonalStep.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/PreferencesStep.tsx","messages":[{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `"`, `“`, `"`, `”`.","line":79,"column":23,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[2556,2697],"text":"\n By clicking "Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"“"},"fix":{"range":[2556,2697],"text":"\n By clicking “Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `“`."},{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[2556,2697],"text":"\n By clicking "Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"”"},"fix":{"range":[2556,2697],"text":"\n By clicking ”Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `”`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `"`, `“`, `"`, `”`.","line":79,"column":38,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"“"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account“, you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `“`."},{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"”"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account”, you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `”`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":79,"column":44,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you‘ll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you’ll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Preferences Step Component\n * Terms acceptance and marketing preferences\n */\n\n\"use client\";\n\nimport Link from \"next/link\";\n\ninterface PreferencesStepProps {\n formData: {\n acceptTerms: boolean;\n marketingConsent: boolean;\n };\n errors: {\n acceptTerms?: string;\n };\n onFieldChange: (field: string, value: boolean) => void;\n onFieldBlur: (field: string) => void;\n loading?: boolean;\n}\n\nexport function PreferencesStep({\n formData,\n errors,\n onFieldChange,\n onFieldBlur,\n loading = false,\n}: PreferencesStepProps) {\n return (\n
\n
\n
\n onFieldChange(\"acceptTerms\", e.target.checked)}\n onBlur={() => onFieldBlur(\"acceptTerms\")}\n disabled={loading}\n className=\"mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n />\n
\n \n {errors.acceptTerms && (\n

{errors.acceptTerms}

\n )}\n
\n
\n\n
\n onFieldChange(\"marketingConsent\", e.target.checked)}\n disabled={loading}\n className=\"mt-1 rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n />\n \n
\n
\n\n
\n

Almost done!

\n

\n By clicking \"Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n

\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":126,"column":27,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":126,"endColumn":30,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3703,3706],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3703,3706],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":128,"column":12,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":128,"endColumn":15,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3799,3802],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3799,3802],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":133,"column":32,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":133,"endColumn":35,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3870,3873],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3870,3873],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any`.","line":134,"column":5,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":134,"endColumn":74},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any`.","line":134,"column":53,"nodeType":"ChainExpression","messageId":"unsafeReturn","endLine":134,"endColumn":67},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [key] on an `any` value.","line":134,"column":63,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":134,"endColumn":66},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":138,"column":32,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":138,"endColumn":35,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[4027,4030],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[4027,4030],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":138,"column":58,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":138,"endColumn":61,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[4053,4056],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[4053,4056],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":141,"column":11,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":144,"endColumn":12},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [key] on an `any` value.","line":142,"column":20,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":142,"endColumn":23},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [key] on an `any` value.","line":142,"column":34,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":142,"endColumn":37},{"ruleId":"@typescript-eslint/no-unsafe-return","severity":2,"message":"Unsafe return of a value of type `any`.","line":143,"column":7,"nodeType":"ReturnStatement","messageId":"unsafeReturn","endLine":143,"endColumn":27},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [key] on an `any` value.","line":143,"column":22,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":143,"endColumn":25},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":145,"column":5,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":145,"endColumn":28},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [lastKey] on an `any` value.","line":145,"column":12,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":145,"endColumn":19},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":150,"column":28,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":150,"endColumn":31,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[4388,4391],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[4388,4391],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-argument","severity":2,"message":"Unsafe argument of type `any` assigned to a parameter of type `string`.","line":161,"column":36,"nodeType":"Identifier","messageId":"unsafeArgument","endLine":161,"endColumn":41},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useCallback has a missing dependency: 'validationConfig'. Either include it or remove the dependency array.","line":164,"column":5,"nodeType":"ArrayExpression","endLine":164,"endColumn":24,"suggestions":[{"desc":"Update the dependencies array to be: [formData.password, validationConfig]","fix":{"range":[4877,4896],"text":"[formData.password, validationConfig]"}}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":169,"column":28,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":169,"endColumn":31,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[4996,4999],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[4996,4999],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":175,"column":11,"nodeType":"AssignmentExpression","messageId":"anyAssignment","endLine":175,"endColumn":42},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":175,"column":23,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":175,"endColumn":26,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[5194,5197],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[5194,5197],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [field] on an `any` value.","line":175,"column":28,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":175,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":206,"column":13,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":208,"endColumn":35},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":208,"column":24,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":208,"endColumn":27,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[6334,6337],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[6334,6337],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [field] on an `any` value.","line":208,"column":29,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":208,"endColumn":34},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":230,"column":15,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":232,"endColumn":37},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":232,"column":26,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":232,"endColumn":29,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[7184,7187],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[7184,7187],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access [field] on an `any` value.","line":232,"column":31,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":232,"endColumn":36},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":373,"column":18,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":373,"endColumn":32}],"suppressedMessages":[],"errorCount":28,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Signup Form Component\n * Refactored multi-step signup form using smaller components\n */\n\n\"use client\";\n\nimport { useState, useCallback } from \"react\";\nimport Link from \"next/link\";\nimport { ErrorMessage } from \"@/components/ui\";\nimport { useSignup } from \"../../hooks/use-auth\";\nimport { validationRules, validateField } from \"@/lib/form-validation\";\nimport type { SignupData } from \"@/lib/auth/api\";\n\nimport { MultiStepForm, type FormStep } from \"./MultiStepForm\";\nimport { AccountStep } from \"./AccountStep\";\nimport { PersonalStep } from \"./PersonalStep\";\nimport { AddressStep } from \"./AddressStep\";\nimport { PreferencesStep } from \"./PreferencesStep\";\n\ninterface SignupFormProps {\n onSuccess?: () => void;\n onError?: (error: string) => void;\n showLoginLink?: boolean;\n className?: string;\n}\n\ninterface SignupFormData {\n email: string;\n password: string;\n confirmPassword: string;\n firstName: string;\n lastName: string;\n company?: string;\n phone?: string;\n sfNumber: string;\n address: {\n line1: string;\n line2?: string;\n city: string;\n state: string;\n postalCode: string;\n country: string;\n };\n nationality?: string;\n dateOfBirth?: string;\n gender?: \"male\" | \"female\" | \"other\";\n acceptTerms: boolean;\n marketingConsent: boolean;\n}\n\ninterface FormErrors {\n [key: string]: string | undefined;\n}\n\nexport function SignupForm({\n onSuccess,\n onError,\n showLoginLink = true,\n className = \"\",\n}: SignupFormProps) {\n const { signup, loading, error, clearError } = useSignup();\n\n const [formData, setFormData] = useState({\n email: \"\",\n password: \"\",\n confirmPassword: \"\",\n firstName: \"\",\n lastName: \"\",\n company: \"\",\n phone: \"\",\n sfNumber: \"\",\n address: {\n line1: \"\",\n line2: \"\",\n city: \"\",\n state: \"\",\n postalCode: \"\",\n country: \"US\",\n },\n nationality: \"\",\n dateOfBirth: \"\",\n gender: undefined,\n acceptTerms: false,\n marketingConsent: false,\n });\n\n const [errors, setErrors] = useState({});\n const [touched, setTouched] = useState>({});\n const [currentStepIndex, setCurrentStepIndex] = useState(0);\n\n // Validation rules\n const validationConfig = {\n email: [\n validationRules.required(\"Email is required\"),\n validationRules.email(\"Please enter a valid email address\"),\n ],\n password: [\n validationRules.required(\"Password is required\"),\n validationRules.minLength(8, \"Password must be at least 8 characters\"),\n validationRules.pattern(\n /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)/,\n \"Password must contain at least one uppercase letter, one lowercase letter, and one number\"\n ),\n ],\n confirmPassword: [validationRules.required(\"Please confirm your password\")],\n firstName: [\n validationRules.required(\"First name is required\"),\n validationRules.minLength(2, \"First name must be at least 2 characters\"),\n ],\n lastName: [\n validationRules.required(\"Last name is required\"),\n validationRules.minLength(2, \"Last name must be at least 2 characters\"),\n ],\n sfNumber: [\n validationRules.required(\"SF Number is required\"),\n validationRules.minLength(6, \"SF Number must be at least 6 characters\"),\n ],\n \"address.line1\": [validationRules.required(\"Address line 1 is required\")],\n \"address.city\": [validationRules.required(\"City is required\")],\n \"address.state\": [validationRules.required(\"State/Province is required\")],\n \"address.postalCode\": [validationRules.required(\"Postal code is required\")],\n \"address.country\": [validationRules.required(\"Country is required\")],\n acceptTerms: [\n {\n validate: (value: any) => value === true,\n message: \"You must accept the terms and conditions\",\n } as any,\n ],\n };\n\n // Get nested value\n const getNestedValue = (obj: any, path: string) => {\n return path.split(\".\").reduce((current, key) => current?.[key], obj);\n };\n\n // Set nested value\n const setNestedValue = (obj: any, path: string, value: any) => {\n const keys = path.split(\".\");\n const lastKey = keys.pop()!;\n const target = keys.reduce((current, key) => {\n if (!current[key]) current[key] = {};\n return current[key];\n }, obj);\n target[lastKey] = value;\n };\n\n // Validate field\n const validateFormField = useCallback(\n (field: string, value: any) => {\n const rules = validationConfig[field as keyof typeof validationConfig];\n if (!rules) return null;\n\n // Special validation for confirm password\n if (field === \"confirmPassword\") {\n if (!value) return \"Please confirm your password\";\n if (value !== formData.password) return \"Passwords do not match\";\n return null;\n }\n\n const result = validateField(value, rules);\n return result.isValid ? null : result.errors[0];\n },\n [formData.password]\n );\n\n // Handle field change\n const handleFieldChange = useCallback(\n (field: string, value: any) => {\n setFormData(prev => {\n const newData = { ...prev };\n if (field.includes(\".\")) {\n setNestedValue(newData, field, value);\n } else {\n (newData as any)[field] = value;\n }\n return newData;\n });\n\n // Clear general error when user starts typing\n if (errors.general) {\n setErrors(prev => ({ ...prev, general: undefined }));\n clearError();\n }\n\n // Validate field if it has been touched\n if (touched[field]) {\n const fieldError = validateFormField(field, value);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n }\n\n // Also validate confirm password when password changes\n if (field === \"password\" && touched.confirmPassword) {\n const confirmPasswordError = validateFormField(\"confirmPassword\", formData.confirmPassword);\n setErrors(prev => ({ ...prev, confirmPassword: confirmPasswordError || undefined }));\n }\n },\n [errors.general, touched, validateFormField, clearError, formData.confirmPassword]\n );\n\n // Handle field blur\n const handleFieldBlur = useCallback(\n (field: string) => {\n setTouched(prev => ({ ...prev, [field]: true }));\n\n const fieldValue = field.includes(\".\")\n ? getNestedValue(formData, field)\n : (formData as any)[field];\n const fieldError = validateFormField(field, fieldValue);\n setErrors(prev => ({ ...prev, [field]: fieldError || undefined }));\n },\n [formData, validateFormField]\n );\n\n // Validate step\n const validateStep = useCallback(\n (stepIndex: number) => {\n const stepFields = [\n [\"email\", \"password\", \"confirmPassword\"], // Account\n [\"firstName\", \"lastName\", \"sfNumber\"], // Personal\n [\"address.line1\", \"address.city\", \"address.state\", \"address.postalCode\", \"address.country\"], // Address\n [\"acceptTerms\"], // Preferences\n ];\n\n const fields = stepFields[stepIndex];\n const stepErrors: FormErrors = {};\n let isValid = true;\n\n fields.forEach(field => {\n const fieldValue = field.includes(\".\")\n ? getNestedValue(formData, field)\n : (formData as any)[field];\n const fieldError = validateFormField(field, fieldValue);\n if (fieldError) {\n stepErrors[field] = fieldError;\n isValid = false;\n }\n });\n\n setErrors(prev => ({ ...prev, ...stepErrors }));\n return isValid;\n },\n [formData, validateFormField]\n );\n\n // Handle form submission\n const handleSubmit = useCallback(async () => {\n // Validate all steps\n const allValid = [0, 1, 2, 3].every(stepIndex => validateStep(stepIndex));\n\n if (!allValid) {\n return;\n }\n\n try {\n const signupData: SignupData = {\n email: formData.email.trim(),\n password: formData.password,\n firstName: formData.firstName.trim(),\n lastName: formData.lastName.trim(),\n company: formData.company?.trim() || undefined,\n phone: formData.phone?.trim() || undefined,\n sfNumber: formData.sfNumber.trim(),\n address: {\n line1: formData.address.line1.trim(),\n line2: formData.address.line2?.trim() || undefined,\n city: formData.address.city.trim(),\n state: formData.address.state.trim(),\n postalCode: formData.address.postalCode.trim(),\n country: formData.address.country,\n },\n nationality: formData.nationality?.trim() || undefined,\n dateOfBirth: formData.dateOfBirth || undefined,\n gender: formData.gender || undefined,\n };\n\n await signup(signupData);\n onSuccess?.();\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : \"Signup failed\";\n setErrors(prev => ({ ...prev, general: errorMessage }));\n onError?.(errorMessage);\n }\n }, [formData, validateStep, signup, onSuccess, onError]);\n\n // Handle step change\n const handleStepChange = useCallback(\n (stepIndex: number) => {\n setCurrentStepIndex(stepIndex);\n // Validate current step when moving to next\n if (stepIndex > currentStepIndex) {\n validateStep(currentStepIndex);\n }\n },\n [currentStepIndex, validateStep]\n );\n\n // Define steps\n const steps: FormStep[] = [\n {\n key: \"account\",\n title: \"Account Details\",\n description: \"Create your account credentials\",\n component: (\n \n ),\n isValid: validateStep(0),\n },\n {\n key: \"personal\",\n title: \"Personal Information\",\n description: \"Tell us about yourself\",\n component: (\n \n ),\n isValid: validateStep(1),\n },\n {\n key: \"address\",\n title: \"Address & Details\",\n description: \"Your address and additional information\",\n component: (\n \n ),\n isValid: validateStep(2),\n },\n {\n key: \"preferences\",\n title: \"Preferences\",\n description: \"Set your preferences\",\n component: (\n \n ),\n isValid: validateStep(3),\n },\n ];\n\n return (\n
\n {/* General Error */}\n {(errors.general || error) && (\n \n {errors.general || error}\n \n )}\n\n \n\n {/* Login Link */}\n {showLoginLink && (\n
\n Already have an account? \n \n Sign in\n \n
\n )}\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/SignupForm/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/hooks/use-auth.ts","messages":[{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":152,"column":5,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":152,"endColumn":17,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[3281,3281],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[3281,3281],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":160,"column":11,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":160,"endColumn":28,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[3462,3462],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[3462,3462],"text":"await "},"desc":"Add await operator."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Authentication Hooks\n * Custom hooks for authentication functionality\n */\n\n\"use client\";\n\nimport { useCallback, useEffect } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { useAuthStore } from \"../services/auth.store\";\nimport { getPostLoginRedirect } from \"../utils/route-protection\";\nimport type { LoginRequest, SignupRequest } from \"@/lib/types\";\n\n/**\n * Main authentication hook\n */\nexport function useAuth() {\n const router = useRouter();\n const searchParams = useSearchParams();\n const store = useAuthStore();\n\n // Enhanced login with redirect handling\n const login = useCallback(\n async (credentials: LoginRequest) => {\n await store.login(credentials);\n const redirectTo = getPostLoginRedirect(searchParams);\n router.push(redirectTo);\n },\n [store, router, searchParams]\n );\n\n // Enhanced signup with redirect handling\n const signup = useCallback(\n async (data: SignupRequest) => {\n await store.signup(data);\n const redirectTo = getPostLoginRedirect(searchParams);\n router.push(redirectTo);\n },\n [store, router, searchParams]\n );\n\n // Enhanced logout with redirect\n const logout = useCallback(async () => {\n await store.logout();\n router.push(\"/auth/login\");\n }, [store, router]);\n\n return {\n // State\n isAuthenticated: store.isAuthenticated,\n user: store.user,\n loading: store.loading,\n error: store.error,\n\n // Actions\n login,\n signup,\n logout,\n requestPasswordReset: store.requestPasswordReset,\n resetPassword: store.resetPassword,\n changePassword: store.changePassword,\n checkPasswordNeeded: store.checkPasswordNeeded,\n linkWhmcs: store.linkWhmcs,\n setPassword: store.setPassword,\n checkAuth: store.checkAuth,\n refreshSession: store.refreshSession,\n clearError: store.clearError,\n };\n}\n\n/**\n * Hook for login functionality\n */\nexport function useLogin() {\n const { login, loading, error, clearError } = useAuth();\n\n return {\n login,\n loading,\n error,\n clearError,\n };\n}\n\n/**\n * Hook for signup functionality\n */\nexport function useSignup() {\n const { signup, loading, error, clearError } = useAuth();\n\n return {\n signup,\n loading,\n error,\n clearError,\n };\n}\n\n/**\n * Hook for password reset functionality\n */\nexport function usePasswordReset() {\n const { requestPasswordReset, resetPassword, loading, error, clearError } = useAuth();\n\n return {\n requestPasswordReset,\n resetPassword,\n loading,\n error,\n clearError,\n };\n}\n\n/**\n * Hook for password change functionality\n */\nexport function usePasswordChange() {\n const { changePassword, loading, error, clearError } = useAuth();\n\n return {\n changePassword,\n loading,\n error,\n clearError,\n };\n}\n\n/**\n * Hook for WHMCS linking functionality\n */\nexport function useWhmcsLink() {\n const { checkPasswordNeeded, linkWhmcs, setPassword, loading, error, clearError } = useAuth();\n\n return {\n checkPasswordNeeded,\n linkWhmcs,\n setPassword,\n loading,\n error,\n clearError,\n };\n}\n\n/**\n * Hook for session management\n */\nexport function useSession() {\n const { isAuthenticated, user, checkAuth, refreshSession, logout } = useAuth();\n\n // Auto-check auth on mount\n useEffect(() => {\n checkAuth();\n }, [checkAuth]);\n\n // Auto-refresh session periodically\n useEffect(() => {\n if (isAuthenticated) {\n const interval = setInterval(\n () => {\n refreshSession();\n },\n 5 * 60 * 1000\n ); // Check every 5 minutes\n\n return () => clearInterval(interval);\n }\n\n return undefined;\n }, [isAuthenticated, refreshSession]);\n\n return {\n isAuthenticated,\n user,\n checkAuth,\n refreshSession,\n logout,\n };\n}\n\n/**\n * Hook for user profile information\n */\nexport function useUser() {\n const { user, isAuthenticated } = useAuth();\n\n const fullName = user ? `${user.firstName || \"\"} ${user.lastName || \"\"}`.trim() : \"\";\n const initials = user\n ? `${user.firstName?.[0] || \"\"}${user.lastName?.[0] || \"\"}`.toUpperCase()\n : \"\";\n\n return {\n user,\n isAuthenticated,\n fullName,\n initials,\n email: user?.email,\n company: user?.company,\n phone: user?.phone,\n avatar: user?.avatar,\n preferences: user?.preferences,\n };\n}\n\n/**\n * Hook for checking user permissions\n */\nexport function usePermissions() {\n const { user } = useAuth();\n\n const hasRole = useCallback(\n (role: string) => {\n return user?.roles.some(r => r.name === role) || false;\n },\n [user]\n );\n\n const hasPermission = useCallback(\n (resource: string, action: string) => {\n return user?.permissions.some(p => p.resource === resource && p.action === action) || false;\n },\n [user]\n );\n\n const hasAnyRole = useCallback(\n (roles: string[]) => {\n return roles.some(role => hasRole(role));\n },\n [hasRole]\n );\n\n const hasAnyPermission = useCallback(\n (permissions: Array<{ resource: string; action: string }>) => {\n return permissions.some(({ resource, action }) => hasPermission(resource, action));\n },\n [hasPermission]\n );\n\n return {\n roles: user?.roles || [],\n permissions: user?.permissions || [],\n hasRole,\n hasPermission,\n hasAnyRole,\n hasAnyPermission,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/services/auth.service.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'AuthErrorCode' is defined but never used.","line":16,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":16,"endColumn":16},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":56,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":56,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":83,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":83,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":94,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":94,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":116,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":116,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":141,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":141,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":153,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":153,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":180,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":180,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":192,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":192,"endColumn":40},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":214,"column":13,"nodeType":"CallExpression","messageId":"object","endLine":214,"endColumn":40},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":236,"column":41,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":236,"endColumn":44,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[5965,5968],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[5965,5968],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":238,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":238,"endColumn":21},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .id on an `any` value.","line":238,"column":19,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":238,"endColumn":21},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":239,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":239,"endColumn":27},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .email on an `any` value.","line":239,"column":22,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":239,"endColumn":27},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":240,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":240,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .firstName on an `any` value.","line":240,"column":26,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":240,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":241,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":241,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .lastName on an `any` value.","line":241,"column":25,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":241,"endColumn":33},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":242,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":242,"endColumn":31},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .company on an `any` value.","line":242,"column":24,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":242,"endColumn":31},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":243,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":243,"endColumn":27},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .phone on an `any` value.","line":243,"column":22,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":243,"endColumn":27},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":244,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":244,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .avatar on an `any` value.","line":244,"column":23,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":244,"endColumn":29},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":271,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":271,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .createdAt on an `any` value.","line":271,"column":26,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":271,"endColumn":35},{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":272,"column":7,"nodeType":"Property","messageId":"anyAssignment","endLine":272,"endColumn":63},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .updatedAt on an `any` value.","line":272,"column":26,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":272,"endColumn":35},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'token' is defined but never used.","line":279,"column":32,"nodeType":null,"messageId":"unusedVar","endLine":279,"endColumn":37}],"suppressedMessages":[],"errorCount":28,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Authentication Service\n * Centralized authentication business logic and API interactions\n */\n\nimport { authAPI } from \"@/lib/auth/api\";\nimport type {\n AuthUser,\n AuthTokens,\n LoginRequest,\n SignupRequest,\n ForgotPasswordRequest,\n ResetPasswordRequest,\n ChangePasswordRequest,\n AuthError,\n AuthErrorCode,\n} from \"@/lib/types\";\n\nexport class AuthService {\n private static instance: AuthService;\n\n static getInstance(): AuthService {\n if (!AuthService.instance) {\n AuthService.instance = new AuthService();\n }\n return AuthService.instance;\n }\n\n /**\n * Create SSO link (e.g., to WHMCS destinations)\n */\n async createSsoLink(destination: string): Promise<{ url: string }> {\n const { apiClient } = await import(\"@/lib/api/client\");\n const res = await apiClient.post<{ url: string }>(\"/auth/sso-link\", { destination });\n return res.data as { url: string };\n }\n\n /**\n * Login user with email and password\n */\n async login(credentials: LoginRequest): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n try {\n const response = await authAPI.login({\n email: credentials.email,\n password: credentials.password,\n });\n\n return {\n user: this.mapApiUserToAuthUser(response.user),\n tokens: {\n accessToken: response.access_token,\n expiresAt: this.calculateTokenExpiry(response.access_token),\n },\n };\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Register new user\n */\n async signup(data: SignupRequest): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n try {\n const response = await authAPI.signup({\n email: data.email,\n password: data.password,\n firstName: data.firstName,\n lastName: data.lastName,\n company: data.company,\n phone: data.phone,\n sfNumber: \"\", // This should be handled by the form\n });\n\n return {\n user: this.mapApiUserToAuthUser(response.user),\n tokens: {\n accessToken: response.access_token,\n expiresAt: this.calculateTokenExpiry(response.access_token),\n },\n };\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Request password reset\n */\n async requestPasswordReset(data: ForgotPasswordRequest): Promise {\n try {\n await authAPI.requestPasswordReset({ email: data.email });\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Reset password with token\n */\n async resetPassword(data: ResetPasswordRequest): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n try {\n const response = await authAPI.resetPassword({\n token: data.token,\n password: data.password,\n });\n\n return {\n user: this.mapApiUserToAuthUser(response.user),\n tokens: {\n accessToken: response.access_token,\n expiresAt: this.calculateTokenExpiry(response.access_token),\n },\n };\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Change user password\n */\n async changePassword(\n token: string,\n data: ChangePasswordRequest\n ): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n try {\n const response = await authAPI.changePassword(token, {\n currentPassword: data.currentPassword,\n newPassword: data.newPassword,\n });\n\n return {\n user: this.mapApiUserToAuthUser(response.user),\n tokens: {\n accessToken: response.access_token,\n expiresAt: this.calculateTokenExpiry(response.access_token),\n },\n };\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Get current user profile\n */\n async getProfile(token: string): Promise {\n try {\n const user = await authAPI.getProfile(token);\n return this.mapApiUserToAuthUser(user);\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Logout user\n */\n async logout(token: string): Promise {\n try {\n await authAPI.logout(token);\n } catch (error) {\n // Don't throw on logout errors, just log them\n console.warn(\"Logout API call failed:\", error);\n }\n }\n\n /**\n * Check if password is needed for WHMCS linking\n */\n async checkPasswordNeeded(email: string): Promise<{\n needsPasswordSet: boolean;\n userExists: boolean;\n email?: string;\n }> {\n try {\n return await authAPI.checkPasswordNeeded({ email });\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Link WHMCS account\n */\n async linkWhmcs(email: string, password: string): Promise<{ needsPasswordSet: boolean }> {\n try {\n const response = await authAPI.linkWhmcs({ email, password });\n return { needsPasswordSet: response.needsPasswordSet };\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Set password for WHMCS linked account\n */\n async setPassword(\n email: string,\n password: string\n ): Promise<{ user: AuthUser; tokens: AuthTokens }> {\n try {\n const response = await authAPI.setPassword({ email, password });\n\n return {\n user: this.mapApiUserToAuthUser(response.user),\n tokens: {\n accessToken: response.access_token,\n expiresAt: this.calculateTokenExpiry(response.access_token),\n },\n };\n } catch (error) {\n throw this.handleAuthError(error);\n }\n }\n\n /**\n * Check if token is expired\n */\n isTokenExpired(expiresAt: string): boolean {\n return new Date(expiresAt) <= new Date();\n }\n\n /**\n * Check if token will expire soon (within 5 minutes)\n */\n isTokenExpiringSoon(expiresAt: string): boolean {\n const fiveMinutesFromNow = new Date(Date.now() + 5 * 60 * 1000);\n return new Date(expiresAt) <= fiveMinutesFromNow;\n }\n\n /**\n * Map API user response to AuthUser type\n */\n private mapApiUserToAuthUser(apiUser: any): AuthUser {\n return {\n id: apiUser.id,\n email: apiUser.email,\n firstName: apiUser.firstName,\n lastName: apiUser.lastName,\n company: apiUser.company,\n phone: apiUser.phone,\n avatar: apiUser.avatar,\n roles: [], // TODO: Map from API when available\n permissions: [], // TODO: Map from API when available\n preferences: {\n theme: \"system\",\n language: \"en\",\n timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n notifications: {\n email: true,\n push: true,\n sms: false,\n categories: {\n billing: true,\n security: true,\n marketing: false,\n system: true,\n },\n },\n dashboard: {\n layout: \"grid\",\n widgets: [],\n defaultView: \"dashboard\",\n },\n },\n lastLoginAt: new Date().toISOString(),\n emailVerified: true, // TODO: Get from API when available\n mfaEnabled: false, // TODO: Get from API when available\n createdAt: apiUser.createdAt || new Date().toISOString(),\n updatedAt: apiUser.updatedAt || new Date().toISOString(),\n };\n }\n\n /**\n * Calculate token expiry time (default to 1 hour if not provided)\n */\n private calculateTokenExpiry(token: string): string {\n // In a real implementation, you would decode the JWT to get the expiry\n // For now, default to 1 hour from now\n return new Date(Date.now() + 60 * 60 * 1000).toISOString();\n }\n\n /**\n * Handle and normalize authentication errors\n */\n private handleAuthError(error: unknown): AuthError {\n if (error instanceof Error) {\n // Map common error messages to error codes\n const message = error.message.toLowerCase();\n\n if (message.includes(\"invalid credentials\") || message.includes(\"unauthorized\")) {\n return {\n code: \"INVALID_CREDENTIALS\",\n message: \"Invalid email or password\",\n };\n }\n\n if (message.includes(\"account locked\") || message.includes(\"locked\")) {\n return {\n code: \"ACCOUNT_LOCKED\",\n message: \"Account has been locked due to too many failed attempts\",\n };\n }\n\n if (message.includes(\"email not verified\")) {\n return {\n code: \"EMAIL_NOT_VERIFIED\",\n message: \"Please verify your email address before logging in\",\n };\n }\n\n if (message.includes(\"token expired\") || message.includes(\"expired\")) {\n return {\n code: \"TOKEN_EXPIRED\",\n message: \"Your session has expired. Please log in again\",\n };\n }\n\n if (message.includes(\"rate limit\") || message.includes(\"too many\")) {\n return {\n code: \"RATE_LIMITED\",\n message: \"Too many attempts. Please try again later\",\n };\n }\n\n return {\n code: \"INVALID_CREDENTIALS\",\n message: error.message,\n };\n }\n\n return {\n code: \"INVALID_CREDENTIALS\",\n message: \"An unexpected error occurred\",\n };\n }\n}\n\nexport const authService = AuthService.getInstance();\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/services/auth.store.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'error' is defined but never used.","line":265,"column":18,"nodeType":null,"messageId":"unusedVar","endLine":265,"endColumn":23},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise returned in function argument where a void return was expected.","line":316,"column":22,"nodeType":"ArrowFunctionExpression","messageId":"voidReturnArgument","endLine":316,"endColumn":45},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":331,"column":9,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":331,"endColumn":24,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[9545,9545],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[9545,9545],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":333,"column":9,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":333,"endColumn":32,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[9584,9584],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[9584,9584],"text":"await "},"desc":"Add await operator."}]}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Authentication Store\n * Centralized authentication state management with Zustand\n */\n\nimport { create } from \"zustand\";\nimport { persist, createJSONStorage } from \"zustand/middleware\";\nimport { authService } from \"./auth.service\";\nimport type { AuthTokens, LoginRequest, SignupRequest } from \"@/lib/types\";\nimport type {\n AuthUser,\n ForgotPasswordRequest,\n ResetPasswordRequest,\n ChangePasswordRequest,\n AuthError,\n} from \"@/lib/types/auth.types\";\n\ninterface AuthState {\n isAuthenticated: boolean;\n user: AuthUser | null;\n tokens: AuthTokens | null;\n loading: boolean;\n error: string | null;\n}\n\ninterface AuthStoreState extends AuthState {\n // Actions\n login: (credentials: LoginRequest) => Promise;\n signup: (data: SignupRequest) => Promise;\n logout: () => Promise;\n requestPasswordReset: (data: ForgotPasswordRequest) => Promise;\n resetPassword: (data: ResetPasswordRequest) => Promise;\n changePassword: (data: ChangePasswordRequest) => Promise;\n checkPasswordNeeded: (email: string) => Promise<{\n needsPasswordSet: boolean;\n userExists: boolean;\n email?: string;\n }>;\n linkWhmcs: (email: string, password: string) => Promise<{ needsPasswordSet: boolean }>;\n setPassword: (email: string, password: string) => Promise;\n\n // Session management\n checkAuth: () => Promise;\n refreshSession: () => Promise;\n clearError: () => void;\n\n // Internal state management\n setLoading: (loading: boolean) => void;\n setError: (error: string | null) => void;\n setUser: (user: AuthUser | null) => void;\n setTokens: (tokens: AuthTokens | null) => void;\n}\n\nexport const useAuthStore = create()(\n persist(\n (set, get) => ({\n // Initial state\n isAuthenticated: false,\n user: null,\n tokens: null,\n loading: false,\n error: null,\n\n // Authentication actions\n login: async (credentials: LoginRequest) => {\n set({ loading: true, error: null });\n try {\n const { user, tokens } = await authService.login(credentials);\n set({\n user,\n tokens,\n isAuthenticated: true,\n loading: false,\n error: null,\n });\n } catch (error) {\n const authError = error as AuthError;\n set({\n loading: false,\n error: authError.message,\n isAuthenticated: false,\n user: null,\n tokens: null,\n });\n throw error;\n }\n },\n\n signup: async (data: SignupRequest) => {\n set({ loading: true, error: null });\n try {\n const { user, tokens } = await authService.signup(data);\n set({\n user,\n tokens,\n isAuthenticated: true,\n loading: false,\n error: null,\n });\n } catch (error) {\n const authError = error as AuthError;\n set({\n loading: false,\n error: authError.message,\n isAuthenticated: false,\n user: null,\n tokens: null,\n });\n throw error;\n }\n },\n\n logout: async () => {\n const { tokens } = get();\n\n // Call logout API if we have tokens\n if (tokens?.accessToken) {\n try {\n await authService.logout(tokens.accessToken);\n } catch (error) {\n console.warn(\"Logout API call failed:\", error);\n // Continue with local logout even if API call fails\n }\n }\n\n set({\n user: null,\n tokens: null,\n isAuthenticated: false,\n loading: false,\n error: null,\n });\n },\n\n requestPasswordReset: async (data: ForgotPasswordRequest) => {\n set({ loading: true, error: null });\n try {\n await authService.requestPasswordReset(data);\n set({ loading: false });\n } catch (error) {\n const authError = error as AuthError;\n set({ loading: false, error: authError.message });\n throw error;\n }\n },\n\n resetPassword: async (data: ResetPasswordRequest) => {\n set({ loading: true, error: null });\n try {\n const { user, tokens } = await authService.resetPassword(data);\n set({\n user,\n tokens,\n isAuthenticated: true,\n loading: false,\n error: null,\n });\n } catch (error) {\n const authError = error as AuthError;\n set({\n loading: false,\n error: authError.message,\n isAuthenticated: false,\n user: null,\n tokens: null,\n });\n throw error;\n }\n },\n\n changePassword: async (data: ChangePasswordRequest) => {\n const { tokens } = get();\n if (!tokens?.accessToken) {\n throw new Error(\"Not authenticated\");\n }\n\n set({ loading: true, error: null });\n try {\n const { user, tokens: newTokens } = await authService.changePassword(\n tokens.accessToken,\n data\n );\n set({\n user,\n tokens: newTokens,\n loading: false,\n error: null,\n });\n } catch (error) {\n const authError = error as AuthError;\n set({ loading: false, error: authError.message });\n throw error;\n }\n },\n\n checkPasswordNeeded: async (email: string) => {\n set({ loading: true, error: null });\n try {\n const result = await authService.checkPasswordNeeded(email);\n set({ loading: false });\n return result;\n } catch (error) {\n const authError = error as AuthError;\n set({ loading: false, error: authError.message });\n throw error;\n }\n },\n\n linkWhmcs: async (email: string, password: string) => {\n set({ loading: true, error: null });\n try {\n const result = await authService.linkWhmcs(email, password);\n set({ loading: false });\n return result;\n } catch (error) {\n const authError = error as AuthError;\n set({ loading: false, error: authError.message });\n throw error;\n }\n },\n\n setPassword: async (email: string, password: string) => {\n set({ loading: true, error: null });\n try {\n const { user, tokens } = await authService.setPassword(email, password);\n set({\n user,\n tokens,\n isAuthenticated: true,\n loading: false,\n error: null,\n });\n } catch (error) {\n const authError = error as AuthError;\n set({\n loading: false,\n error: authError.message,\n isAuthenticated: false,\n user: null,\n tokens: null,\n });\n throw error;\n }\n },\n\n // Session management\n checkAuth: async () => {\n const { tokens } = get();\n\n if (!tokens?.accessToken) {\n set({ isAuthenticated: false, loading: false, user: null, tokens: null });\n return;\n }\n\n // Check if token is expired\n if (authService.isTokenExpired(tokens.expiresAt)) {\n set({ isAuthenticated: false, loading: false, user: null, tokens: null });\n return;\n }\n\n set({ loading: true });\n try {\n const user = await authService.getProfile(tokens.accessToken);\n set({ user, isAuthenticated: true, loading: false, error: null });\n } catch (error) {\n // Token is invalid, clear auth state\n console.info(\"Token validation failed, clearing auth state\");\n set({\n user: null,\n tokens: null,\n isAuthenticated: false,\n loading: false,\n error: null,\n });\n }\n },\n\n refreshSession: async () => {\n const { tokens, checkAuth } = get();\n\n if (!tokens?.accessToken) {\n return;\n }\n\n // Check if token needs refresh (expires within 5 minutes)\n if (authService.isTokenExpiringSoon(tokens.expiresAt)) {\n // For now, just re-validate the token\n // In a real implementation, you would call a refresh token endpoint\n await checkAuth();\n }\n },\n\n // Utility actions\n clearError: () => set({ error: null }),\n\n setLoading: (loading: boolean) => set({ loading }),\n\n setError: (error: string | null) => set({ error }),\n\n setUser: (user: AuthUser | null) => set({ user }),\n\n setTokens: (tokens: AuthTokens | null) => set({ tokens }),\n }),\n {\n name: \"auth-store\",\n storage: createJSONStorage(() => localStorage),\n partialize: state => ({\n user: state.user,\n tokens: state.tokens,\n isAuthenticated: state.isAuthenticated,\n }),\n // Rehydrate the store and check auth status\n onRehydrateStorage: () => state => {\n if (state?.tokens?.accessToken) {\n // Check auth status after rehydration\n setTimeout(() => state.checkAuth(), 0);\n }\n },\n }\n )\n);\n\n// Session timeout detection\nlet sessionTimeoutId: NodeJS.Timeout | null = null;\n\nexport const startSessionTimeout = () => {\n const checkSession = () => {\n const state = useAuthStore.getState();\n if (state.tokens?.accessToken) {\n if (authService.isTokenExpired(state.tokens.expiresAt)) {\n state.logout();\n } else {\n state.refreshSession();\n }\n }\n };\n\n // Check session every minute\n sessionTimeoutId = setInterval(checkSession, 60 * 1000);\n};\n\nexport const stopSessionTimeout = () => {\n if (sessionTimeoutId) {\n clearInterval(sessionTimeoutId);\n sessionTimeoutId = null;\n }\n};\n\n// Auto-start session timeout when store is created\nif (typeof window !== \"undefined\") {\n startSessionTimeout();\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/services/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/types/index.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":31,"column":13,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":31,"endColumn":16,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[582,585],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[582,585],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":33,"column":41,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":33,"endColumn":44,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[673,676],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[673,676],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Authentication Types\n * Type definitions specific to the authentication feature\n */\n\n// Re-export common auth types from lib\nexport type {\n AuthUser,\n AuthTokens,\n AuthState,\n AuthError,\n AuthErrorCode,\n LoginRequest,\n SignupRequest,\n ForgotPasswordRequest,\n ResetPasswordRequest,\n ChangePasswordRequest,\n UserRole,\n Permission,\n UserPreferences,\n} from \"@/lib/types\";\n\n// Feature-specific types\nexport interface AuthFormProps {\n onSuccess?: () => void;\n onError?: (error: string) => void;\n className?: string;\n}\n\nexport interface AuthStepProps {\n formData: any;\n errors: Record;\n onFieldChange: (field: string, value: any) => void;\n onFieldBlur: (field: string) => void;\n loading?: boolean;\n}\n\nexport interface AuthGuardConfig {\n requireAuth?: boolean;\n roles?: string[];\n permissions?: Array<{ resource: string; action: string }>;\n fallback?: React.ReactNode;\n requireAll?: boolean;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/utils/auth-guard.tsx","messages":[{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":35,"column":5,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":35,"endColumn":17,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[898,898],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[898,898],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":192,"column":36,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[4509,4563],"text":"You don't have the required role to view this content."},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[4509,4563],"text":"You don‘t have the required role to view this content."},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[4509,4563],"text":"You don't have the required role to view this content."},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[4509,4563],"text":"You don’t have the required role to view this content."},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":206,"column":36,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[4864,4911],"text":"You don't have permission to view this content."},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[4864,4911],"text":"You don‘t have permission to view this content."},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[4864,4911],"text":"You don't have permission to view this content."},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[4864,4911],"text":"You don’t have permission to view this content."},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Authentication Guard Components\n * Components for protecting routes and handling authentication state\n */\n\n\"use client\";\n\nimport { useEffect, ReactNode } from \"react\";\nimport { useRouter, usePathname } from \"next/navigation\";\nimport { useAuth } from \"../hooks/use-auth\";\nimport { getAuthRedirect, isProtectedRoute } from \"./route-protection\";\nimport { LoadingSpinner } from \"@/components/ui\";\n\ninterface AuthGuardProps {\n children: ReactNode;\n fallback?: ReactNode;\n requireAuth?: boolean;\n}\n\n/**\n * Auth Guard Component\n * Protects routes based on authentication status\n */\nexport function AuthGuard({\n children,\n fallback = ,\n requireAuth = true,\n}: AuthGuardProps) {\n const { isAuthenticated, loading, checkAuth } = useAuth();\n const router = useRouter();\n const pathname = usePathname();\n\n useEffect(() => {\n // Check authentication status on mount\n checkAuth();\n }, [checkAuth]);\n\n useEffect(() => {\n // Handle redirects based on auth status and current route\n if (!loading) {\n const redirectTo = getAuthRedirect(isAuthenticated, pathname);\n if (redirectTo) {\n router.push(redirectTo);\n return;\n }\n }\n }, [isAuthenticated, loading, pathname, router]);\n\n // Show loading while checking authentication\n if (loading) {\n return <>{fallback};\n }\n\n // For protected routes, ensure user is authenticated\n if (requireAuth && isProtectedRoute(pathname) && !isAuthenticated) {\n return <>{fallback};\n }\n\n // For auth pages, redirect authenticated users\n if (isAuthenticated && pathname.startsWith(\"/auth/\")) {\n return <>{fallback};\n }\n\n return <>{children};\n}\n\n/**\n * Protected Route Component\n * Wrapper for routes that require authentication\n */\nexport function ProtectedRoute({\n children,\n fallback,\n}: {\n children: ReactNode;\n fallback?: ReactNode;\n}) {\n return (\n \n {children}\n \n );\n}\n\n/**\n * Public Route Component\n * Wrapper for routes that don't require authentication\n */\nexport function PublicRoute({ children, fallback }: { children: ReactNode; fallback?: ReactNode }) {\n return (\n \n {children}\n \n );\n}\n\n/**\n * Default fallback component for auth guard\n */\nfunction AuthGuardFallback() {\n return (\n
\n
\n \n

Checking authentication...

\n
\n
\n );\n}\n\n/**\n * Role-based Guard Component\n * Protects content based on user roles\n */\ninterface RoleGuardProps {\n children: ReactNode;\n roles: string[];\n fallback?: ReactNode;\n requireAll?: boolean;\n}\n\nexport function RoleGuard({\n children,\n roles,\n fallback = ,\n requireAll = false,\n}: RoleGuardProps) {\n const { user, isAuthenticated } = useAuth();\n\n if (!isAuthenticated || !user) {\n return <>{fallback};\n }\n\n const userRoles = user.roles.map(role => role.name);\n const hasAccess = requireAll\n ? roles.every(role => userRoles.includes(role))\n : roles.some(role => userRoles.includes(role));\n\n if (!hasAccess) {\n return <>{fallback};\n }\n\n return <>{children};\n}\n\n/**\n * Permission-based Guard Component\n * Protects content based on user permissions\n */\ninterface PermissionGuardProps {\n children: ReactNode;\n permissions: Array<{ resource: string; action: string }>;\n fallback?: ReactNode;\n requireAll?: boolean;\n}\n\nexport function PermissionGuard({\n children,\n permissions,\n fallback = ,\n requireAll = false,\n}: PermissionGuardProps) {\n const { user, isAuthenticated } = useAuth();\n\n if (!isAuthenticated || !user) {\n return <>{fallback};\n }\n\n const hasAccess = requireAll\n ? permissions.every(({ resource, action }) =>\n user.permissions.some(p => p.resource === resource && p.action === action)\n )\n : permissions.some(({ resource, action }) =>\n user.permissions.some(p => p.resource === resource && p.action === action)\n );\n\n if (!hasAccess) {\n return <>{fallback};\n }\n\n return <>{children};\n}\n\n/**\n * Default fallback for role guard\n */\nfunction RoleGuardFallback() {\n return (\n
\n
\n

Access Denied

\n

You don't have the required role to view this content.

\n
\n
\n );\n}\n\n/**\n * Default fallback for permission guard\n */\nfunction PermissionGuardFallback() {\n return (\n
\n
\n

Access Denied

\n

You don't have permission to view this content.

\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/utils/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/auth/utils/route-protection.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/BillingStatusBadge/BillingStatusBadge.tsx","messages":[{"ruleId":"@typescript-eslint/no-redundant-type-constituents","severity":2,"message":"\"Draft\" | \"Pending\" | \"Paid\" | \"Unpaid\" | \"Overdue\" | \"Cancelled\" | \"Refunded\" | \"Collections\" is overridden by string in this union type.","line":16,"column":11,"nodeType":"TSTypeReference","messageId":"literalOverridden","endLine":16,"endColumn":24}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport {\n CheckCircleIcon,\n ExclamationTriangleIcon,\n ClockIcon,\n DocumentTextIcon,\n XCircleIcon,\n} from \"@heroicons/react/24/outline\";\nimport { StatusPill } from \"@/components/ui/status-pill\";\nimport type { StatusPillProps } from \"@/components/ui/status-pill\";\nimport type { InvoiceStatus } from \"@customer-portal/shared\";\n\ninterface BillingStatusBadgeProps extends Omit {\n status: InvoiceStatus | string;\n showIcon?: boolean;\n}\n\nconst getStatusConfig = (status: string) => {\n switch (status.toLowerCase()) {\n case \"paid\":\n return {\n variant: \"success\" as const,\n icon: ,\n label: \"Paid\",\n };\n case \"overdue\":\n return {\n variant: \"error\" as const,\n icon: ,\n label: \"Overdue\",\n };\n case \"unpaid\":\n return {\n variant: \"warning\" as const,\n icon: ,\n label: \"Unpaid\",\n };\n case \"cancelled\":\n case \"canceled\":\n return {\n variant: \"neutral\" as const,\n icon: ,\n label: \"Cancelled\",\n };\n case \"draft\":\n return {\n variant: \"neutral\" as const,\n icon: ,\n label: \"Draft\",\n };\n case \"refunded\":\n return {\n variant: \"info\" as const,\n icon: ,\n label: \"Refunded\",\n };\n case \"collections\":\n return {\n variant: \"error\" as const,\n icon: ,\n label: \"Collections\",\n };\n case \"payment pending\":\n return {\n variant: \"warning\" as const,\n icon: ,\n label: \"Payment Pending\",\n };\n default:\n return {\n variant: \"neutral\" as const,\n icon: ,\n label: status,\n };\n }\n};\n\nconst BillingStatusBadge = forwardRef(\n ({ status, showIcon = true, children, ...props }, ref) => {\n const config = getStatusConfig(status);\n\n return (\n \n );\n }\n);\n\nBillingStatusBadge.displayName = \"BillingStatusBadge\";\n\nexport { BillingStatusBadge };\nexport type { BillingStatusBadgeProps };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/BillingStatusBadge/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/BillingSummary/BillingSummary.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'BillingStatusBadge' is defined but never used.","line":12,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":12,"endColumn":28}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport Link from \"next/link\";\nimport {\n CreditCardIcon,\n ExclamationTriangleIcon,\n CheckCircleIcon,\n ClockIcon,\n ArrowRightIcon,\n} from \"@heroicons/react/24/outline\";\nimport { BillingStatusBadge } from \"../BillingStatusBadge\";\nimport type { BillingSummaryData } from \"../../types\";\nimport { formatCurrency, getCurrencyLocale } from \"@/utils/currency\";\nimport { cn } from \"@/lib/utils\";\n\ninterface BillingSummaryProps extends React.HTMLAttributes {\n summary: BillingSummaryData;\n loading?: boolean;\n compact?: boolean;\n}\n\nconst BillingSummary = forwardRef(\n ({ summary, loading = false, compact = false, className, ...props }, ref) => {\n if (loading) {\n return (\n \n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n
\n \n );\n }\n\n const formatAmount = (amount: number) => {\n return formatCurrency(amount, {\n currency: summary.currency,\n currencySymbol: summary.currencySymbol,\n locale: getCurrencyLocale(summary.currency),\n });\n };\n\n const summaryItems = [\n {\n label: \"Outstanding\",\n amount: summary.totalOutstanding,\n count: summary.invoiceCount.unpaid,\n variant: summary.totalOutstanding > 0 ? \"warning\" : \"neutral\",\n icon: summary.totalOutstanding > 0 ? ClockIcon : CheckCircleIcon,\n },\n {\n label: \"Overdue\",\n amount: summary.totalOverdue,\n count: summary.invoiceCount.overdue,\n variant: summary.totalOverdue > 0 ? \"error\" : \"neutral\",\n icon: summary.totalOverdue > 0 ? ExclamationTriangleIcon : CheckCircleIcon,\n },\n {\n label: \"Paid This Period\",\n amount: summary.totalPaid,\n count: summary.invoiceCount.paid,\n variant: \"success\",\n icon: CheckCircleIcon,\n },\n ];\n\n return (\n \n {/* Header */}\n
\n
\n
\n \n
\n

Billing Summary

\n
\n {!compact && (\n \n View All\n \n \n )}\n
\n\n {/* Summary Items */}\n
\n {summaryItems.map((item, index) => {\n const IconComponent = item.icon;\n\n return (\n \n
\n \n
\n
{item.label}
\n {!compact && item.count > 0 && (\n
\n {item.count} invoice{item.count !== 1 ? \"s\" : \"\"}\n
\n )}\n
\n
\n
\n
\n {formatAmount(item.amount)}\n
\n {compact && item.count > 0 && (\n
\n {item.count} invoice{item.count !== 1 ? \"s\" : \"\"}\n
\n )}\n
\n
\n );\n })}\n \n\n {/* Total Invoices */}\n {!compact && (\n
\n
\n Total Invoices\n {summary.invoiceCount.total}\n
\n
\n )}\n\n {/* Quick Actions */}\n {compact && (\n
\n \n View All Invoices\n \n \n
\n )}\n \n );\n }\n);\n\nBillingSummary.displayName = \"BillingSummary\";\n\nexport { BillingSummary };\nexport type { BillingSummaryProps };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/BillingSummary/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceHeader.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Insert `··`","line":57,"column":15,"nodeType":null,"messageId":"insert","endLine":57,"endColumn":15,"fix":{"range":[1479,1479],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `··`","line":58,"column":1,"nodeType":null,"messageId":"insert","endLine":58,"endColumn":1,"fix":{"range":[1489,1489],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `····`","line":59,"column":15,"nodeType":null,"messageId":"insert","endLine":59,"endColumn":15,"fix":{"range":[1547,1547],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `····`","line":60,"column":1,"nodeType":null,"messageId":"insert","endLine":60,"endColumn":1,"fix":{"range":[1559,1559],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·size=\"sm\"·variant=\"gray\"·className=\"mr-1.5\"·label=\"Downloading...\"` with `⏎··················size=\"sm\"⏎··················variant=\"gray\"⏎··················className=\"mr-1.5\"⏎··················label=\"Downloading...\"⏎···············`","line":70,"column":32,"nodeType":null,"messageId":"replace","endLine":70,"endColumn":99,"fix":{"range":[2085,2152],"text":"\n size=\"sm\"\n variant=\"gray\"\n className=\"mr-1.5\"\n label=\"Downloading...\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·size=\"sm\"·variant=\"gray\"·className=\"mr-1.5\"·label=\"Loading...\"` with `⏎······················size=\"sm\"⏎······················variant=\"gray\"⏎······················className=\"mr-1.5\"⏎······················label=\"Loading...\"⏎···················`","line":85,"column":36,"nodeType":null,"messageId":"replace","endLine":85,"endColumn":99,"fix":{"range":[2855,2918],"text":"\n size=\"sm\"\n variant=\"gray\"\n className=\"mr-1.5\"\n label=\"Loading...\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·size=\"sm\"·variant=\"white\"·className=\"mr-2\"·label=\"Processing...\"` with `⏎······················size=\"sm\"⏎······················variant=\"white\"⏎······················className=\"mr-2\"⏎······················label=\"Processing...\"⏎···················`","line":102,"column":36,"nodeType":null,"messageId":"replace","endLine":102,"endColumn":101,"fix":{"range":[3722,3787],"text":"\n size=\"sm\"\n variant=\"white\"\n className=\"mr-2\"\n label=\"Processing...\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `··`","line":128,"column":23,"nodeType":null,"messageId":"insert","endLine":128,"endColumn":23,"fix":{"range":[4862,4862],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `··`","line":129,"column":1,"nodeType":null,"messageId":"insert","endLine":129,"endColumn":1,"fix":{"range":[4918,4918],"text":" "}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":9,"fixableErrorCount":0,"fixableWarningCount":9,"source":"\"use client\";\n\nimport React from \"react\";\nimport { DetailHeader } from \"@/components/common/DetailHeader\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport {\n ArrowTopRightOnSquareIcon,\n ArrowDownTrayIcon,\n ServerIcon,\n} from \"@heroicons/react/24/outline\";\nimport { format } from \"date-fns\";\nimport type { Invoice } from \"@customer-portal/shared\";\n\nconst formatDate = (dateString?: string) => {\n if (!dateString || dateString === \"0000-00-00\" || dateString === \"0000-00-00 00:00:00\")\n return \"N/A\";\n try {\n const date = new Date(dateString);\n if (isNaN(date.getTime())) return \"N/A\";\n return format(date, \"MMM d, yyyy\");\n } catch {\n return \"N/A\";\n }\n};\n\ninterface InvoiceHeaderProps {\n invoice: Invoice;\n loadingDownload?: boolean;\n loadingPayment?: boolean;\n loadingPaymentMethods?: boolean;\n onDownload?: () => void;\n onPay?: () => void;\n onManagePaymentMethods?: () => void;\n}\n\nexport function InvoiceHeader(props: InvoiceHeaderProps) {\n const {\n invoice,\n loadingDownload,\n loadingPayment,\n loadingPaymentMethods,\n onDownload,\n onPay,\n onManagePaymentMethods,\n } = props;\n\n return (\n
\n \n \n {loadingDownload ? (\n \n ) : (\n \n )}\n Download\n \n\n {(invoice.status === \"Unpaid\" || invoice.status === \"Overdue\") && (\n <>\n \n {loadingPaymentMethods ? (\n \n ) : (\n \n )}\n Payment\n \n\n \n {loadingPayment ? (\n \n ) : (\n \n )}\n {invoice.status === \"Overdue\" ? \"Pay Overdue\" : \"Pay Now\"}\n \n \n )}\n
\n }\n meta={\n
\n
\n Issued:\n \n {formatDate(invoice.issuedAt)}\n \n
\n {invoice.dueDate && (\n
\n Due:\n \n {formatDate(invoice.dueDate)}\n {invoice.status === \"Overdue\" && \" • OVERDUE\"}\n \n
\n )}\n
\n }\n />\n \n );\n}\n\nexport type { InvoiceHeaderProps };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceItems.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":48,"column":1,"nodeType":null,"messageId":"delete","endLine":49,"endColumn":1,"fix":{"range":[1553,1554],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"\"use client\";\n\nimport React from \"react\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { formatCurrency } from \"@/utils/currency\";\nimport type { InvoiceItem } from \"@customer-portal/shared\";\n\ninterface InvoiceItemsProps {\n items?: InvoiceItem[];\n currency: string;\n}\n\nexport function InvoiceItems({ items = [], currency }: InvoiceItemsProps) {\n return (\n \n {items.length > 0 ? (\n
\n {items.map(item => (\n \n
\n
{item.description}
\n {item.quantity && item.quantity > 1 && (\n
Quantity: {item.quantity}
\n )}\n {item.serviceId && (\n
Service ID: {item.serviceId}
\n )}\n
\n
\n
\n {formatCurrency(item.amount || 0, { currency })}\n
\n
\n
\n ))}\n \n ) : (\n
No items found on this invoice.
\n )}\n
\n );\n}\n\nexport type { InvoiceItemsProps };\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceDetail/InvoiceTotals.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":43,"column":1,"nodeType":null,"messageId":"delete","endLine":44,"endColumn":1,"fix":{"range":[1356,1357],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"\"use client\";\n\nimport React from \"react\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { formatCurrency } from \"@/utils/currency\";\n\ninterface InvoiceTotalsProps {\n subtotal: number;\n tax: number;\n total: number;\n currency: string;\n}\n\nexport function InvoiceTotals({ subtotal, tax, total, currency }: InvoiceTotalsProps) {\n const fmt = (amount: number) => formatCurrency(amount, { currency });\n return (\n \n
\n
\n
\n Subtotal\n {fmt(subtotal)}\n
\n {tax > 0 && (\n
\n Tax\n {fmt(tax)}\n
\n )}\n
\n
\n Total\n {fmt(total)}\n
\n
\n
\n
\n
\n );\n}\n\nexport type { InvoiceTotalsProps };\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceDetail/index.ts","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":4,"column":1,"nodeType":null,"messageId":"delete","endLine":5,"endColumn":1,"fix":{"range":[98,99],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"export * from \"./InvoiceHeader\";\nexport * from \"./InvoiceItems\";\nexport * from \"./InvoiceTotals\";\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceList/InvoiceList.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'showFilters' is assigned a value but never used.","line":25,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":25,"endColumn":14},{"ruleId":"react-hooks/rules-of-hooks","severity":2,"message":"React Hook \"useSubscriptionInvoices\" is called conditionally. React Hooks must be called in the exact same order in every component render.","line":36,"column":7,"nodeType":"Identifier","endLine":36,"endColumn":30},{"ruleId":"@typescript-eslint/no-unnecessary-type-assertion","severity":2,"message":"This assertion is unnecessary since it does not change the type of the expression.","line":36,"column":31,"nodeType":"TSAsExpression","messageId":"unnecessaryAssertion","endLine":36,"endColumn":55,"fix":{"range":[1319,1329],"text":""}},{"ruleId":"react-hooks/rules-of-hooks","severity":2,"message":"React Hook \"useInvoices\" is called conditionally. React Hooks must be called in the exact same order in every component render.","line":37,"column":7,"nodeType":"Identifier","endLine":37,"endColumn":18},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·page:·currentPage,·limit:·pageSize,·status:·statusFilter·===·\"all\"·?·undefined·:·statusFilter` with `⏎········page:·currentPage,⏎········limit:·pageSize,⏎········status:·statusFilter·===·\"all\"·?·undefined·:·statusFilter,⏎·····`","line":37,"column":20,"nodeType":null,"messageId":"replace","endLine":37,"endColumn":114,"fix":{"range":[1390,1484],"text":"\n page: currentPage,\n limit: pageSize,\n status: statusFilter === \"all\" ? undefined : statusFilter,\n "}},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"The 'invoices' logical expression could make the dependencies of useMemo Hook (at line 57) change on every render. Move it inside the useMemo callback. Alternatively, wrap the initialization of 'invoices' in its own useMemo() Hook.","line":45,"column":9,"nodeType":"VariableDeclarator","endLine":45,"endColumn":40}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":1,"fixableWarningCount":1,"source":"\"use client\";\n\nimport React, { useMemo, useState } from \"react\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { ErrorState } from \"@/components/ui/error-state\";\nimport { SearchFilterBar } from \"@/components/common/SearchFilterBar\";\nimport { PaginationBar } from \"@/components/common/PaginationBar\";\nimport { InvoiceTable } from \"@/features/billing/components/InvoiceTable/InvoiceTable\";\nimport { useInvoices } from \"@/features/billing/hooks/useBilling\";\nimport { useSubscriptionInvoices } from \"@/features/subscriptions/hooks/useSubscriptions\";\nimport type { Invoice } from \"@customer-portal/shared\";\n\ninterface InvoicesListProps {\n subscriptionId?: number;\n pageSize?: number;\n showFilters?: boolean;\n compact?: boolean;\n className?: string;\n}\n\nexport function InvoicesList({\n subscriptionId,\n pageSize = 10,\n showFilters = true,\n compact = false,\n className,\n}: InvoicesListProps) {\n const [searchTerm, setSearchTerm] = useState(\"\");\n const [statusFilter, setStatusFilter] = useState(\"all\");\n const [currentPage, setCurrentPage] = useState(1);\n\n const isSubscriptionMode = typeof subscriptionId === \"number\" && !isNaN(subscriptionId);\n\n const invoicesQuery = isSubscriptionMode\n ? useSubscriptionInvoices(subscriptionId as number, { page: currentPage, limit: pageSize })\n : useInvoices({ page: currentPage, limit: pageSize, status: statusFilter === \"all\" ? undefined : statusFilter });\n\n const { data, isLoading, error } = invoicesQuery as {\n data?: { invoices: Invoice[]; pagination?: { totalItems: number; totalPages: number } };\n isLoading: boolean;\n error: unknown;\n };\n\n const invoices = data?.invoices || [];\n const pagination = data?.pagination;\n\n const filtered = useMemo(() => {\n if (!searchTerm) return invoices;\n const term = searchTerm.toLowerCase();\n return invoices.filter(inv => {\n return (\n inv.number.toLowerCase().includes(term) ||\n (inv.description ? inv.description.toLowerCase().includes(term) : false)\n );\n });\n }, [invoices, searchTerm]);\n\n const statusFilterOptions = [\n { value: \"all\", label: \"All Status\" },\n { value: \"Unpaid\", label: \"Unpaid\" },\n { value: \"Paid\", label: \"Paid\" },\n { value: \"Overdue\", label: \"Overdue\" },\n { value: \"Cancelled\", label: \"Cancelled\" },\n ];\n\n if (isLoading) {\n return (\n \n
\n
\n \n

Loading invoices...

\n
\n
\n
\n );\n }\n\n if (error) {\n return (\n \n \n \n );\n }\n\n return (\n {\n setStatusFilter(value);\n setCurrentPage(1);\n }}\n filterOptions={isSubscriptionMode ? undefined : statusFilterOptions}\n filterLabel={isSubscriptionMode ? undefined : \"Filter by status\"}\n />\n }\n headerClassName=\"bg-gray-50 rounded md:p-2 p-1 mb-1\"\n footer={\n pagination && filtered.length > 0 ? (\n \n ) : undefined\n }\n className={className}\n >\n \n \n );\n}\n\nexport type { InvoicesListProps };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceList/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceTable/InvoiceTable.tsx","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe spread of an `any` value in an array.","line":157,"column":13,"nodeType":"SpreadElement","messageId":"unsafeArraySpread","endLine":157,"endColumn":24}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport Link from \"next/link\";\nimport { format } from \"date-fns\";\nimport {\n DocumentTextIcon,\n ArrowTopRightOnSquareIcon,\n CheckCircleIcon,\n ExclamationTriangleIcon,\n ClockIcon,\n} from \"@heroicons/react/24/outline\";\nimport { DataTable } from \"@/components/common/DataTable\";\nimport { BillingStatusBadge } from \"../BillingStatusBadge\";\nimport type { Invoice } from \"@customer-portal/shared\";\nimport { formatCurrency, getCurrencyLocale } from \"@/utils/currency\";\nimport { cn } from \"@/lib/utils\";\n\ninterface InvoiceTableProps {\n invoices: Invoice[];\n loading?: boolean;\n onInvoiceClick?: (invoice: Invoice) => void;\n showActions?: boolean;\n compact?: boolean;\n className?: string;\n}\n\nconst getStatusIcon = (status: string) => {\n switch (status.toLowerCase()) {\n case \"paid\":\n return ;\n case \"unpaid\":\n return ;\n case \"overdue\":\n return ;\n case \"cancelled\":\n case \"canceled\":\n return ;\n default:\n return ;\n }\n};\n\nexport function InvoiceTable({\n invoices,\n loading = false,\n onInvoiceClick,\n showActions = true,\n compact = false,\n className,\n}: InvoiceTableProps) {\n const router = useRouter();\n\n const handleInvoiceClick = (invoice: Invoice) => {\n if (onInvoiceClick) {\n onInvoiceClick(invoice);\n } else {\n router.push(`/billing/invoices/${invoice.id}`);\n }\n };\n\n const columns = useMemo(() => {\n const baseColumns = [\n {\n key: \"invoice\",\n header: \"Invoice\",\n render: (invoice: Invoice) => (\n
\n {getStatusIcon(invoice.status)}\n
\n
{invoice.number}
\n {!compact && invoice.description && (\n
{invoice.description}
\n )}\n
\n
\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (invoice: Invoice) => ,\n },\n {\n key: \"amount\",\n header: \"Amount\",\n render: (invoice: Invoice) => (\n \n {formatCurrency(invoice.total, {\n currency: invoice.currency,\n currencySymbol: invoice.currencySymbol,\n locale: getCurrencyLocale(invoice.currency),\n })}\n \n ),\n },\n ];\n\n // Add date columns if not compact\n if (!compact) {\n baseColumns.push(\n {\n key: \"invoiceDate\",\n header: \"Invoice Date\",\n render: (invoice: Invoice) => (\n \n {invoice.issuedAt ? format(new Date(invoice.issuedAt), \"MMM d, yyyy\") : \"N/A\"}\n \n ),\n },\n {\n key: \"dueDate\",\n header: \"Due Date\",\n render: (invoice: Invoice) => (\n \n {invoice.dueDate ? format(new Date(invoice.dueDate), \"MMM d, yyyy\") : \"N/A\"}\n \n ),\n }\n );\n }\n\n // Add actions column if enabled\n if (showActions) {\n baseColumns.push({\n key: \"actions\",\n header: \"\",\n render: (invoice: Invoice) => (\n
\n e.stopPropagation()}\n >\n View\n \n \n
\n ),\n });\n }\n\n return baseColumns;\n }, [compact, showActions]);\n\n const emptyState = {\n icon: ,\n title: \"No invoices found\",\n description: \"No invoices have been generated yet.\",\n };\n\n if (loading) {\n return (\n
\n
\n {[...Array(5)].map((_, i) => (\n
\n ))}\n
\n
\n );\n }\n\n return (\n \n );\n}\n\nexport type { InvoiceTableProps };\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/InvoiceTable/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/PaymentMethodCard/PaymentMethodCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/PaymentMethodCard/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/containers/InvoiceDetail.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ExclamationTriangleIcon' is defined but never used.","line":9,"column":27,"nodeType":null,"messageId":"unusedVar","endLine":9,"endColumn":50},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·InvoiceHeader,·InvoiceItems,·InvoiceTotals·` with `⏎··InvoiceHeader,⏎··InvoiceItems,⏎··InvoiceTotals,⏎`","line":16,"column":9,"nodeType":null,"messageId":"replace","endLine":16,"endColumn":53,"fix":{"range":[754,798],"text":"\n InvoiceHeader,\n InvoiceItems,\n InvoiceTotals,\n"}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon={}·title=\"Invoice\"·description=\"Invoice·details·and·actions\"` with `⏎········icon={}⏎········title=\"Invoice\"⏎········description=\"Invoice·details·and·actions\"⏎······`","line":64,"column":18,"nodeType":null,"messageId":"replace","endLine":64,"endColumn":102,"fix":{"range":[2531,2615],"text":"\n icon={}\n title=\"Invoice\"\n description=\"Invoice details and actions\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon={}·title=\"Invoice\"·description=\"Invoice·details·and·actions\"` with `⏎········icon={}⏎········title=\"Invoice\"⏎········description=\"Invoice·details·and·actions\"⏎······`","line":77,"column":18,"nodeType":null,"messageId":"replace","endLine":77,"endColumn":102,"fix":{"range":[2957,3041],"text":"\n icon={}\n title=\"Invoice\"\n description=\"Invoice details and actions\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·href=\"/billing/invoices\"·className=\"inline-flex·items-center·text-gray-600·hover:text-gray-900·transition-colors\"` with `⏎············href=\"/billing/invoices\"⏎············className=\"inline-flex·items-center·text-gray-600·hover:text-gray-900·transition-colors\"⏎··········`","line":96,"column":16,"nodeType":null,"messageId":"replace","endLine":96,"endColumn":130,"fix":{"range":[3573,3687],"text":"\n href=\"/billing/invoices\"\n className=\"inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·subtotal={invoice.subtotal}·tax={invoice.tax}·total={invoice.total}·currency={invoice.currency}` with `⏎··············subtotal={invoice.subtotal}⏎··············tax={invoice.tax}⏎··············total={invoice.total}⏎··············currency={invoice.currency}⏎···········`","line":124,"column":27,"nodeType":null,"messageId":"replace","endLine":124,"endColumn":123,"fix":{"range":[4853,4949],"text":"\n subtotal={invoice.subtotal}\n tax={invoice.tax}\n total={invoice.total}\n currency={invoice.currency}\n "}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":5,"source":"\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { useParams } from \"next/navigation\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { ErrorState } from \"@/components/ui/error-state\";\nimport { CheckCircleIcon, ExclamationTriangleIcon } from \"@heroicons/react/24/outline\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { CreditCardIcon } from \"@heroicons/react/24/outline\";\nimport { logger } from \"@/lib/logger\";\nimport { AuthService } from \"@/features/auth/services/auth.service\";\nimport { openSsoLink } from \"@/lib/utils/sso\";\nimport { useInvoice, useCreateInvoiceSsoLink } from \"@/features/billing/hooks\";\nimport { InvoiceHeader, InvoiceItems, InvoiceTotals } from \"@/features/billing/components/InvoiceDetail\";\n\nexport function InvoiceDetailContainer() {\n const params = useParams();\n const [loadingDownload, setLoadingDownload] = useState(false);\n const [loadingPayment, setLoadingPayment] = useState(false);\n const [loadingPaymentMethods, setLoadingPaymentMethods] = useState(false);\n\n const invoiceId = parseInt(params.id as string);\n const createSsoLinkMutation = useCreateInvoiceSsoLink();\n const { data: invoice, isLoading, error } = useInvoice(invoiceId);\n\n const handleCreateSsoLink = (target: \"view\" | \"download\" | \"pay\" = \"view\") => {\n void (async () => {\n if (!invoice) return;\n if (target === \"download\") setLoadingDownload(true);\n else setLoadingPayment(true);\n try {\n const ssoLink = await createSsoLinkMutation.mutateAsync({ invoiceId: invoice.id, target });\n if (target === \"download\") openSsoLink(ssoLink.url, { newTab: false });\n else openSsoLink(ssoLink.url, { newTab: true });\n } catch (err) {\n logger.error(err, \"Failed to create SSO link\");\n } finally {\n if (target === \"download\") setLoadingDownload(false);\n else setLoadingPayment(false);\n }\n })();\n };\n\n const handleManagePaymentMethods = () => {\n void (async () => {\n setLoadingPaymentMethods(true);\n try {\n const sso = await AuthService.getInstance().createSsoLink(\n \"index.php?rp=/account/paymentmethods\"\n );\n openSsoLink(sso.url, { newTab: true });\n } catch (err) {\n logger.error(err, \"Failed to create payment methods SSO link\");\n } finally {\n setLoadingPaymentMethods(false);\n }\n })();\n };\n\n if (isLoading) {\n return (\n } title=\"Invoice\" description=\"Invoice details and actions\">\n
\n
\n \n

Loading invoice...

\n
\n
\n
\n );\n }\n\n if (error || !invoice) {\n return (\n } title=\"Invoice\" description=\"Invoice details and actions\">\n \n
\n \n ← Back to invoices\n \n
\n
\n );\n }\n\n return (\n
\n
\n
\n \n ← Back to Invoices\n \n
\n\n
\n handleCreateSsoLink(\"download\")}\n onPay={() => handleCreateSsoLink(\"pay\")}\n onManagePaymentMethods={handleManagePaymentMethods}\n />\n\n {invoice.status === \"Paid\" && (\n
\n \n
\n Invoice Paid\n • Paid on {invoice.paidDate || invoice.issuedAt}\n
\n
\n )}\n\n
\n \n \n\n {(invoice.status === \"Unpaid\" || invoice.status === \"Overdue\") && (\n \n
\n \n {loadingPaymentMethods ? (\n
\n ) : (\n 💳\n )}\n Payment Methods\n \n handleCreateSsoLink(\"pay\")}\n disabled={loadingPayment}\n className={`inline-flex items-center justify-center px-5 py-2.5 border border-transparent text-sm font-semibold rounded-lg text-white transition-all duration-200 shadow-md whitespace-nowrap ${\n invoice.status === \"Overdue\"\n ? \"bg-red-600 hover:bg-red-700 ring-2 ring-red-200 hover:ring-red-300\"\n : \"bg-blue-600 hover:bg-blue-700 hover:shadow-lg\"\n }`}\n >\n {loadingPayment ? (\n
\n ) : (\n \n )}\n {invoice.status === \"Overdue\" ? \"Pay Overdue\" : \"Pay Now\"}\n \n
\n
\n )}\n
\n
\n
\n
\n );\n}\n\nexport default InvoiceDetailContainer;\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/containers/InvoicesList.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon={}·title=\"Invoices\"·description=\"Manage·and·view·your·billing·invoices\"` with `⏎······icon={}⏎······title=\"Invoices\"⏎······description=\"Manage·and·view·your·billing·invoices\"⏎····`","line":9,"column":16,"nodeType":null,"messageId":"replace","endLine":9,"endColumn":111,"fix":{"range":[293,388],"text":"\n icon={}\n title=\"Invoices\"\n description=\"Manage and view your billing invoices\"\n "}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"\"use client\";\n\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { CreditCardIcon } from \"@heroicons/react/24/outline\";\nimport { InvoicesList } from \"@/features/billing/components/InvoiceList/InvoiceList\";\n\nexport function InvoicesListContainer() {\n return (\n } title=\"Invoices\" description=\"Manage and view your billing invoices\">\n \n \n );\n}\n\nexport default InvoicesListContainer;\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/containers/PaymentMethods.tsx","messages":[{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async method 'refetch' has no 'await' expression.","line":30,"column":5,"nodeType":"ArrowFunctionExpression","messageId":"missingAwait","endLine":30,"endColumn":20,"suggestions":[{"messageId":"removeAsync","fix":{"range":[1236,1242],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":149,"column":28,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[5712,5823],"text":"\n You haven't added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[5712,5823],"text":"\n You haven‘t added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[5712,5823],"text":"\n You haven't added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[5712,5823],"text":"\n You haven’t added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { ErrorState } from \"@/components/ui/error-state\";\nimport { useSession } from \"@/features/auth/hooks\";\nimport { ApiError } from \"@/lib/api/client\";\nimport { AuthService } from \"@/features/auth/services/auth.service\";\nimport { openSsoLink } from \"@/lib/utils/sso\";\nimport { usePaymentRefresh } from \"@/features/billing/hooks/usePaymentRefresh\";\nimport { PaymentMethodCard, usePaymentMethods } from \"@/features/billing\";\nimport { CreditCardIcon, PlusIcon } from \"@heroicons/react/24/outline\";\nimport { InlineToast } from \"@/components/ui/inline-toast\";\nimport { logger } from \"@/lib/logger\";\n\nexport function PaymentMethodsContainer() {\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState(null);\n const { isAuthenticated } = useSession();\n\n const {\n data: paymentMethodsData,\n isLoading: isLoadingPaymentMethods,\n error: paymentMethodsError,\n } = usePaymentMethods();\n\n const paymentRefresh = usePaymentRefresh({\n refetch: async () => ({ data: paymentMethodsData }),\n hasMethods: (data?: { totalCount?: number }) => !!data && (data.totalCount || 0) > 0,\n attachFocusListeners: true,\n });\n\n const openPaymentMethods = async () => {\n try {\n setIsLoading(true);\n setError(null);\n if (!isAuthenticated) {\n setError(\"Please log in to access payment methods.\");\n setIsLoading(false);\n return;\n }\n const sso = await AuthService.getInstance().createSsoLink(\n \"index.php?rp=/account/paymentmethods\"\n );\n openSsoLink(sso.url, { newTab: true });\n setIsLoading(false);\n } catch (error) {\n logger.error(error, \"Failed to open payment methods\");\n if (error instanceof ApiError && error.status === 401)\n setError(\"Authentication failed. Please log in again.\");\n else setError(\"Unable to access payment methods. Please try again later.\");\n setIsLoading(false);\n }\n };\n\n useEffect(() => {\n // Placeholder hook for future logic when returning from WHMCS\n }, [isAuthenticated]);\n\n if (error || paymentMethodsError) {\n const errorMessage =\n error ||\n (paymentMethodsError instanceof Error\n ? paymentMethodsError.message\n : \"An unexpected error occurred\");\n return (\n }\n title=\"Payment Methods\"\n description=\"Manage your saved payment methods and billing information\"\n >\n \n \n );\n }\n\n return (\n }\n title=\"Payment Methods\"\n description=\"Manage your saved payment methods and billing information\"\n >\n \n
\n
\n {isLoadingPaymentMethods ? (\n \n
\n
\n \n

Loading payment methods...

\n
\n
\n
\n ) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? (\n \n

Your Payment Methods

\n {\n void openPaymentMethods();\n }}\n disabled={isLoading}\n className=\"inline-flex items-center gap-2 px-3 py-2 text-sm bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n \n Add New\n \n
\n }\n >\n
\n {paymentMethodsData.paymentMethods.map(paymentMethod => (\n {\n void openPaymentMethods();\n }}\n onDelete={() => {\n void openPaymentMethods();\n }}\n onSetDefault={() => {\n void openPaymentMethods();\n }}\n />\n ))}\n
\n \n ) : (\n \n
\n
\n \n
\n

No Payment Methods

\n

\n You haven't added any payment methods yet. Add one to make payments easier.\n

\n {\n void openPaymentMethods();\n }}\n disabled={isLoading}\n className=\"inline-flex items-center gap-2 px-6 py-3 bg-blue-600 text-white font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors\"\n >\n {isLoading ? (\n <>\n
\n Opening...\n \n ) : (\n <>\n \n Add Payment Method\n \n )}\n \n

Opens in a new tab for security

\n
\n
\n )}\n
\n\n
\n
\n
\n
\n \n
\n
\n

Secure & Encrypted

\n

\n All payment information is securely encrypted and protected with industry-standard\n security.\n

\n
\n
\n
\n\n
\n

Supported Payment Methods

\n
    \n
  • • Credit Cards (Visa, MasterCard, American Express)
  • \n
  • • Debit Cards
  • \n
\n
\n
\n \n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/containers/index.ts","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":4,"column":1,"nodeType":null,"messageId":"delete","endLine":5,"endColumn":1,"fix":{"range":[99,100],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"export * from \"./PaymentMethods\";\nexport * from \"./InvoicesList\";\nexport * from \"./InvoiceDetail\";\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/hooks/useBilling.ts","messages":[{"ruleId":"@typescript-eslint/unbound-method","severity":2,"message":"Avoid referencing unbound methods which may cause unintentional scoping of `this`.\nIf your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead.","line":89,"column":17,"nodeType":"MemberExpression","messageId":"unboundWithoutThisAnnotation","endLine":89,"endColumn":52},{"ruleId":"@typescript-eslint/unbound-method","severity":2,"message":"Avoid referencing unbound methods which may cause unintentional scoping of `this`.\nIf your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead.","line":98,"column":17,"nodeType":"MemberExpression","messageId":"unboundWithoutThisAnnotation","endLine":98,"endColumn":56},{"ruleId":"@typescript-eslint/unbound-method","severity":2,"message":"Avoid referencing unbound methods which may cause unintentional scoping of `this`.\nIf your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead.","line":109,"column":17,"nodeType":"MemberExpression","messageId":"unboundWithoutThisAnnotation","endLine":109,"endColumn":55},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":112,"column":7,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":112,"endColumn":71,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[3089,3089],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[3089,3089],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/unbound-method","severity":2,"message":"Avoid referencing unbound methods which may cause unintentional scoping of `this`.\nIf your function does not access `this`, you can annotate it with `this: void`, or consider using an arrow function instead.","line":124,"column":17,"nodeType":"MemberExpression","messageId":"unboundWithoutThisAnnotation","endLine":124,"endColumn":51},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":127,"column":7,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":127,"endColumn":71,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[3467,3467],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[3467,3467],"text":"await "},"desc":"Add await operator."}]}],"suppressedMessages":[],"errorCount":6,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { useMemo } from \"react\";\nimport { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { BillingService } from \"../services\";\nimport type { InvoiceQueryParams } from \"../services\";\n\n/**\n * Hook for fetching invoices with pagination and filtering\n */\nexport function useInvoices(params: InvoiceQueryParams = {}) {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: [\"invoices\", params],\n queryFn: () => BillingService.getInvoices(params),\n enabled: isAuthenticated && !!token,\n staleTime: 60 * 1000, // 1 minute\n gcTime: 5 * 60 * 1000, // 5 minutes\n });\n}\n\n/**\n * Hook for fetching a single invoice\n */\nexport function useInvoice(invoiceId: number) {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: [\"invoice\", invoiceId],\n queryFn: () => BillingService.getInvoice(invoiceId),\n enabled: isAuthenticated && !!token && !!invoiceId,\n staleTime: 60 * 1000, // 1 minute\n gcTime: 5 * 60 * 1000, // 5 minutes\n });\n}\n\n/**\n * Hook for fetching invoice subscriptions\n */\nexport function useInvoiceSubscriptions(invoiceId: number) {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: [\"invoice-subscriptions\", invoiceId],\n queryFn: () => BillingService.getInvoiceSubscriptions(invoiceId),\n enabled: isAuthenticated && !!token && !!invoiceId,\n staleTime: 60 * 1000, // 1 minute\n gcTime: 5 * 60 * 1000, // 5 minutes\n });\n}\n\n/**\n * Hook for fetching payment methods\n */\nexport function usePaymentMethods() {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: [\"paymentMethods\"],\n queryFn: () => BillingService.getPaymentMethods(),\n enabled: isAuthenticated && !!token,\n staleTime: 1 * 60 * 1000, // 1 minute\n gcTime: 5 * 60 * 1000, // 5 minutes\n retry: 3,\n retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),\n });\n}\n\n/**\n * Hook for fetching payment gateways\n */\nexport function usePaymentGateways() {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: [\"paymentGateways\"],\n queryFn: () => BillingService.getPaymentGateways(),\n enabled: isAuthenticated && !!token,\n staleTime: 60 * 60 * 1000, // 1 hour\n gcTime: 2 * 60 * 60 * 1000, // 2 hours\n });\n}\n\n/**\n * Mutation hook for creating invoice SSO links\n */\nexport function useCreateInvoiceSsoLink() {\n return useMutation({\n mutationFn: BillingService.createInvoiceSsoLink,\n });\n}\n\n/**\n * Mutation hook for creating invoice payment links\n */\nexport function useCreateInvoicePaymentLink() {\n return useMutation({\n mutationFn: BillingService.createInvoicePaymentLink,\n });\n}\n\n/**\n * Mutation hook for setting default payment method\n */\nexport function useSetDefaultPaymentMethod() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: BillingService.setDefaultPaymentMethod,\n onSuccess: () => {\n // Invalidate payment methods to refresh the list\n queryClient.invalidateQueries({ queryKey: [\"paymentMethods\"] });\n },\n });\n}\n\n/**\n * Mutation hook for deleting payment method\n */\nexport function useDeletePaymentMethod() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: BillingService.deletePaymentMethod,\n onSuccess: () => {\n // Invalidate payment methods to refresh the list\n queryClient.invalidateQueries({ queryKey: [\"paymentMethods\"] });\n },\n });\n}\n\n/**\n * Hook for generating billing summary from invoice data\n */\nexport function useBillingSummary(params: InvoiceQueryParams = {}) {\n const { data: invoiceData, ...queryResult } = useInvoices({\n ...params,\n limit: 100, // Get more invoices for summary calculation\n });\n\n const summary = useMemo(() => {\n if (!invoiceData?.invoices) {\n return null;\n }\n\n const invoices = invoiceData.invoices;\n const currency = invoices[0]?.currency || \"USD\";\n const currencySymbol = invoices[0]?.currencySymbol;\n\n const totals = invoices.reduce(\n (acc, invoice) => {\n switch (invoice.status.toLowerCase()) {\n case \"unpaid\":\n acc.totalOutstanding += invoice.total;\n acc.invoiceCount.unpaid += 1;\n break;\n case \"overdue\":\n acc.totalOverdue += invoice.total;\n acc.invoiceCount.overdue += 1;\n break;\n case \"paid\":\n acc.totalPaid += invoice.total;\n acc.invoiceCount.paid += 1;\n break;\n }\n acc.invoiceCount.total += 1;\n return acc;\n },\n {\n totalOutstanding: 0,\n totalOverdue: 0,\n totalPaid: 0,\n currency,\n currencySymbol,\n invoiceCount: {\n total: 0,\n unpaid: 0,\n overdue: 0,\n paid: 0,\n },\n }\n );\n\n return totals;\n }, [invoiceData]);\n\n return {\n ...queryResult,\n data: summary,\n };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/hooks/usePaymentRefresh.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/services/billing.service.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'PaymentGateway' is defined but never used.","line":8,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":8,"endColumn":17}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { apiClient } from \"@/lib/api/client\";\nimport type {\n Invoice,\n InvoiceList,\n InvoiceSsoLink,\n PaymentMethod,\n PaymentMethodList,\n PaymentGateway,\n PaymentGatewayList,\n InvoicePaymentLink,\n Subscription,\n} from \"@customer-portal/shared\";\n\nexport interface InvoiceQueryParams {\n page?: number;\n limit?: number;\n status?: string;\n}\n\nexport interface CreateInvoiceSsoLinkParams {\n invoiceId: number;\n target?: \"view\" | \"download\" | \"pay\";\n}\n\nexport interface CreateInvoicePaymentLinkParams {\n invoiceId: number;\n paymentMethodId?: number;\n gatewayName?: string;\n}\n\n/**\n * Centralized billing service for all invoice and payment operations\n */\nexport class BillingService {\n /**\n * Fetch paginated list of invoices\n */\n static async getInvoices(params: InvoiceQueryParams = {}): Promise {\n const { page = 1, limit = 10, status } = params;\n\n const searchParams = new URLSearchParams({\n page: page.toString(),\n limit: limit.toString(),\n ...(status && { status }),\n });\n\n const res = await apiClient.get(`/invoices?${searchParams}`);\n return res.data as InvoiceList;\n }\n\n /**\n * Fetch a single invoice by ID\n */\n static async getInvoice(invoiceId: number): Promise {\n const res = await apiClient.get(`/invoices/${invoiceId}`);\n return res.data as Invoice;\n }\n\n /**\n * Fetch subscriptions associated with an invoice\n */\n static async getInvoiceSubscriptions(invoiceId: number): Promise {\n const res = await apiClient.get(`/invoices/${invoiceId}/subscriptions`);\n return res.data as Subscription[];\n }\n\n /**\n * Create SSO link for invoice viewing/downloading/payment\n */\n static async createInvoiceSsoLink(params: CreateInvoiceSsoLinkParams): Promise {\n const { invoiceId, target = \"view\" } = params;\n\n const searchParams = new URLSearchParams();\n if (target !== \"view\") {\n searchParams.append(\"target\", target);\n }\n\n const url = `/invoices/${invoiceId}/sso-link${searchParams.toString() ? `?${searchParams.toString()}` : \"\"}`;\n const res = await apiClient.post(url);\n return res.data as InvoiceSsoLink;\n }\n\n /**\n * Create payment link for invoice\n */\n static async createInvoicePaymentLink(\n params: CreateInvoicePaymentLinkParams\n ): Promise {\n const { invoiceId, paymentMethodId, gatewayName } = params;\n\n const searchParams = new URLSearchParams();\n if (paymentMethodId) {\n searchParams.append(\"paymentMethodId\", paymentMethodId.toString());\n }\n if (gatewayName) {\n searchParams.append(\"gatewayName\", gatewayName);\n }\n\n const url = `/invoices/${invoiceId}/payment-link${searchParams.toString() ? `?${searchParams.toString()}` : \"\"}`;\n const res = await apiClient.post(url);\n return res.data as InvoicePaymentLink;\n }\n\n /**\n * Fetch user's payment methods\n */\n static async getPaymentMethods(): Promise {\n const res = await apiClient.get(\"/invoices/payment-methods\");\n return res.data as PaymentMethodList;\n }\n\n /**\n * Fetch available payment gateways\n */\n static async getPaymentGateways(): Promise {\n const res = await apiClient.get(\"/invoices/payment-gateways\");\n return res.data as PaymentGatewayList;\n }\n\n /**\n * Set a payment method as default\n */\n static async setDefaultPaymentMethod(paymentMethodId: number): Promise {\n const res = await apiClient.patch(\n `/invoices/payment-methods/${paymentMethodId}/default`\n );\n return res.data as PaymentMethod;\n }\n\n /**\n * Delete a payment method\n */\n static async deletePaymentMethod(paymentMethodId: number): Promise {\n await apiClient.delete(`/invoices/payment-methods/${paymentMethodId}`);\n return;\n }\n}\n\nexport default BillingService;\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/services/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/types/billing.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/utils/billing.utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/billing/utils/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/AddonGroup.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/AddressConfirmation.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/AddressForm.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/ConfigurationStep.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'Link' is defined but never used.","line":4,"column":8,"nodeType":null,"messageId":"unusedVar","endLine":4,"endColumn":12}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { ReactNode } from \"react\";\nimport Link from \"next/link\";\nimport {\n CheckCircleIcon,\n ExclamationTriangleIcon,\n InformationCircleIcon,\n} from \"@heroicons/react/24/outline\";\nimport { AnimatedCard } from \"@/components/ui/animated-card\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface StepValidation {\n isValid: boolean;\n errors?: string[];\n warnings?: string[];\n}\n\nexport interface ConfigurationStepProps {\n // Step identification\n stepNumber: number;\n title: string;\n description?: string;\n\n // Step state\n isActive?: boolean;\n isCompleted?: boolean;\n isDisabled?: boolean;\n validation?: StepValidation;\n\n // Content\n children: ReactNode;\n helpText?: string;\n infoText?: string;\n\n // Actions\n onNext?: () => void;\n onPrevious?: () => void;\n onSkip?: () => void;\n nextLabel?: string;\n previousLabel?: string;\n skipLabel?: string;\n showActions?: boolean;\n\n // Styling\n variant?: \"default\" | \"highlighted\" | \"compact\";\n showStepIndicator?: boolean;\n\n // State\n loading?: boolean;\n disabled?: boolean;\n\n // Custom content\n headerContent?: ReactNode;\n footerContent?: ReactNode;\n}\n\nexport function ConfigurationStep({\n stepNumber,\n title,\n description,\n isActive = true,\n isCompleted = false,\n isDisabled = false,\n validation,\n children,\n helpText,\n infoText,\n onNext,\n onPrevious,\n onSkip,\n nextLabel = \"Continue\",\n previousLabel = \"Back\",\n skipLabel = \"Skip\",\n showActions = true,\n variant = \"default\",\n showStepIndicator = true,\n loading = false,\n disabled = false,\n headerContent,\n footerContent,\n}: ConfigurationStepProps) {\n const getStepIndicatorClasses = () => {\n if (isCompleted) {\n return \"bg-green-500 border-green-500 text-white\";\n }\n if (isActive && !isDisabled) {\n return \"border-blue-500 text-blue-500 bg-blue-50\";\n }\n if (isDisabled) {\n return \"border-gray-300 text-gray-400 bg-gray-50\";\n }\n return \"border-gray-300 text-gray-500 bg-white\";\n };\n\n const getCardVariant = () => {\n if (variant === \"highlighted\") return \"highlighted\";\n if (isDisabled) return \"static\";\n return \"default\";\n };\n\n const hasErrors = validation?.errors && validation.errors.length > 0;\n const hasWarnings = validation?.warnings && validation.warnings.length > 0;\n const isValid = validation?.isValid !== false;\n\n return (\n \n {/* Step Header */}\n
\n
\n {/* Step Indicator */}\n {showStepIndicator && (\n \n {isCompleted ? : {stepNumber}}\n
\n )}\n\n {/* Step Title and Description */}\n
\n \n {title}\n \n {description && (\n \n {description}\n

\n )}\n\n {/* Validation Status */}\n {validation && (\n
\n {hasErrors && (\n
\n \n
\n {validation.errors!.map((error, index) => (\n
{error}
\n ))}\n
\n
\n )}\n\n {hasWarnings && !hasErrors && (\n
\n \n
\n {validation.warnings!.map((warning, index) => (\n
{warning}
\n ))}\n
\n
\n )}\n\n {isValid && !hasWarnings && isCompleted && (\n
\n \n Configuration complete\n
\n )}\n
\n )}\n
\n
\n\n {headerContent &&
{headerContent}
}\n \n\n {/* Step Content */}\n {!isDisabled &&
{children}
}\n\n {/* Help Text */}\n {helpText && !isDisabled && (\n
\n
\n \n

{helpText}

\n
\n
\n )}\n\n {/* Info Text */}\n {infoText && !isDisabled && (\n
\n

{infoText}

\n
\n )}\n\n {/* Actions */}\n {showActions && !isDisabled && (\n
\n
\n {onPrevious && (\n \n {previousLabel}\n \n )}\n\n {onSkip && (\n \n {skipLabel}\n \n )}\n
\n\n {onNext && (\n \n {loading ? (\n \n \n \n \n \n Processing...\n \n ) : (\n nextLabel\n )}\n \n )}\n
\n )}\n\n {/* Footer Content */}\n {footerContent &&
{footerContent}
}\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'CurrencyYenIcon' is defined but never used.","line":7,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":7,"endColumn":18},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'serviceItems' is assigned a value but never used.","line":129,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":129,"endColumn":21},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'addonItems' is assigned a value but never used.","line":130,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":130,"endColumn":19},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'installationItems' is assigned a value but never used.","line":131,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":131,"endColumn":26},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'activationItems' is assigned a value but never used.","line":132,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":132,"endColumn":24}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":5,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { ReactNode } from \"react\";\nimport {\n ArrowLeftIcon,\n ArrowRightIcon,\n CurrencyYenIcon,\n InformationCircleIcon,\n} from \"@heroicons/react/24/outline\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface OrderItem {\n id: string;\n name: string;\n sku: string;\n price?: number;\n billingCycle?: \"Monthly\" | \"Onetime\" | \"Annual\";\n type: \"service\" | \"installation\" | \"addon\" | \"activation\";\n description?: string;\n isAutoAdded?: boolean;\n}\n\nexport interface OrderConfiguration {\n label: string;\n value: string;\n important?: boolean;\n}\n\nexport interface OrderTotals {\n monthlyTotal: number;\n oneTimeTotal: number;\n annualTotal?: number;\n discountAmount?: number;\n taxAmount?: number;\n}\n\nexport interface EnhancedOrderSummaryProps {\n // Core order data\n orderItems: OrderItem[];\n totals: OrderTotals;\n\n // Plan information\n planName: string;\n planTier?: string;\n planDescription?: string;\n\n // Configuration details\n configurations?: OrderConfiguration[];\n\n // Additional information\n infoLines?: string[];\n disclaimers?: string[];\n\n // Pricing breakdown control\n showDetailedBreakdown?: boolean;\n showTaxes?: boolean;\n showDiscounts?: boolean;\n\n // Actions\n onContinue?: () => void;\n onBack?: () => void;\n backUrl?: string;\n backLabel?: string;\n continueLabel?: string;\n showActions?: boolean;\n\n // State\n disabled?: boolean;\n loading?: boolean;\n\n // Styling\n variant?: \"simple\" | \"detailed\" | \"checkout\";\n size?: \"compact\" | \"standard\" | \"large\";\n\n // Custom content\n children?: ReactNode;\n headerContent?: ReactNode;\n footerContent?: ReactNode;\n}\n\nexport function EnhancedOrderSummary({\n orderItems,\n totals,\n planName,\n planTier,\n planDescription,\n configurations = [],\n infoLines = [],\n disclaimers = [],\n showDetailedBreakdown = true,\n showTaxes = false,\n showDiscounts = false,\n onContinue,\n onBack,\n backUrl,\n backLabel = \"Back\",\n continueLabel = \"Continue\",\n showActions = true,\n disabled = false,\n loading = false,\n variant = \"detailed\",\n size = \"standard\",\n children,\n headerContent,\n footerContent,\n}: EnhancedOrderSummaryProps) {\n const sizeClasses = {\n compact: \"p-4\",\n standard: \"p-6\",\n large: \"p-8\",\n };\n\n const getVariantClasses = () => {\n switch (variant) {\n case \"checkout\":\n return \"bg-gradient-to-br from-gray-50 to-blue-50 border-2 border-blue-200 shadow-lg\";\n case \"detailed\":\n return \"bg-white border border-gray-200 shadow-md\";\n default:\n return \"bg-white border border-gray-200\";\n }\n };\n\n const formatPrice = (price: number) => price.toLocaleString();\n\n const monthlyItems = orderItems.filter(item => item.billingCycle === \"Monthly\");\n const oneTimeItems = orderItems.filter(item => item.billingCycle === \"Onetime\");\n const serviceItems = orderItems.filter(item => item.type === \"service\");\n const addonItems = orderItems.filter(item => item.type === \"addon\");\n const installationItems = orderItems.filter(item => item.type === \"installation\");\n const activationItems = orderItems.filter(item => item.type === \"activation\");\n\n return (\n \n {/* Header */}\n
\n
\n

Order Summary

\n {variant === \"checkout\" && (\n
\n
\n ¥{formatPrice(totals.monthlyTotal)}/mo\n
\n {totals.oneTimeTotal > 0 && (\n
\n + ¥{formatPrice(totals.oneTimeTotal)} one-time\n
\n )}\n
\n )}\n
\n\n {headerContent}\n
\n\n {/* Plan Information */}\n
\n
\n
\n

\n {planName}\n {planTier && ({planTier})}\n

\n {planDescription &&

{planDescription}

}\n
\n
\n\n {/* Configuration Details */}\n {configurations.length > 0 && (\n
\n {configurations.map((config, index) => (\n
\n \n {config.label}:\n \n \n {config.value}\n \n
\n ))}\n
\n )}\n
\n\n {/* Detailed Pricing Breakdown */}\n {showDetailedBreakdown && variant !== \"simple\" && (\n
\n {/* Monthly Services */}\n {monthlyItems.length > 0 && (\n
\n
Monthly Charges
\n
\n {monthlyItems.map((item, index) => (\n
\n
\n {item.name}\n {item.isAutoAdded && (\n (Auto-added)\n )}\n {item.description && (\n
{item.description}
\n )}\n
\n \n ¥{formatPrice(item.price || 0)}\n \n
\n ))}\n
\n
\n )}\n\n {/* One-time Charges */}\n {oneTimeItems.length > 0 && (\n
\n
One-time Charges
\n
\n {oneTimeItems.map((item, index) => (\n
\n
\n {item.name}\n {item.description && (\n
{item.description}
\n )}\n
\n \n ¥{formatPrice(item.price || 0)}\n \n
\n ))}\n
\n
\n )}\n\n {/* Discounts */}\n {showDiscounts && totals.discountAmount && totals.discountAmount > 0 && (\n
\n
\n Discount Applied\n \n -¥{formatPrice(totals.discountAmount)}\n \n
\n
\n )}\n\n {/* Taxes */}\n {showTaxes && totals.taxAmount && totals.taxAmount > 0 && (\n
\n
\n Tax (10%)\n ¥{formatPrice(totals.taxAmount)}\n
\n
\n )}\n
\n )}\n\n {/* Simple Item List for simple variant */}\n {variant === \"simple\" && (\n
\n {orderItems.map((item, index) => (\n
\n {item.name}\n \n ¥{formatPrice(item.price || 0)}\n {item.billingCycle === \"Monthly\" ? \"/mo\" : \" one-time\"}\n \n
\n ))}\n
\n )}\n\n {/* Totals */}\n
\n
\n
\n Monthly Total:\n ¥{formatPrice(totals.monthlyTotal)}\n
\n\n {totals.oneTimeTotal > 0 && (\n
\n One-time Total:\n ¥{formatPrice(totals.oneTimeTotal)}\n
\n )}\n\n {totals.annualTotal && (\n
\n Annual Total:\n ¥{formatPrice(totals.annualTotal)}\n
\n )}\n
\n
\n\n {/* Info Lines */}\n {infoLines.length > 0 && (\n
\n
\n \n
\n {infoLines.map((line, index) => (\n

\n {line}\n

\n ))}\n
\n
\n
\n )}\n\n {/* Disclaimers */}\n {disclaimers.length > 0 && (\n
\n
\n {disclaimers.map((disclaimer, index) => (\n

\n {disclaimer}\n

\n ))}\n
\n
\n )}\n\n {/* Custom Content */}\n {children &&
{children}
}\n\n {/* Actions */}\n {showActions && (\n
\n {backUrl ? (\n \n \n {backLabel}\n \n ) : onBack ? (\n \n \n {backLabel}\n \n ) : null}\n\n {onContinue && (\n \n )}\n
\n )}\n\n {/* Footer Content */}\n {footerContent &&
{footerContent}
}\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/OrderSummary.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/PaymentForm.tsx","messages":[{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has a missing dependency: 'validatePayment'. Either include it or remove the dependency array.","line":101,"column":6,"nodeType":"ArrayExpression","endLine":101,"endColumn":61,"suggestions":[{"desc":"Update the dependencies array to be: [selectedMethod, existingMethods, requirePaymentMethod, validatePayment]","fix":{"range":[2347,2402],"text":"[selectedMethod, existingMethods, requirePaymentMethod, validatePayment]"}}]},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'brand' is defined but never used.","line":109,"column":29,"nodeType":null,"messageId":"unusedVar","endLine":109,"endColumn":34}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\n\nimport { useState, useEffect } from \"react\";\nimport {\n CreditCardIcon,\n ExclamationTriangleIcon,\n CheckCircleIcon,\n} from \"@heroicons/react/24/outline\";\n\nexport interface PaymentMethod {\n id: string;\n type: \"card\" | \"bank\" | \"paypal\";\n last4?: string;\n brand?: string;\n expiryMonth?: number;\n expiryYear?: number;\n isDefault?: boolean;\n name?: string;\n}\n\nexport interface PaymentFormProps {\n // Payment methods\n existingMethods?: PaymentMethod[];\n selectedMethodId?: string;\n\n // Callbacks\n onMethodSelect?: (methodId: string) => void;\n onAddNewMethod?: () => void;\n onValidationChange?: (isValid: boolean, errors: string[]) => void;\n\n // Configuration\n title?: string;\n description?: string;\n showTitle?: boolean;\n allowNewMethod?: boolean;\n requirePaymentMethod?: boolean;\n\n // State\n loading?: boolean;\n disabled?: boolean;\n\n // Styling\n variant?: \"default\" | \"compact\" | \"inline\";\n\n // Custom content\n children?: React.ReactNode;\n footerContent?: React.ReactNode;\n}\n\nexport function PaymentForm({\n existingMethods = [],\n selectedMethodId,\n onMethodSelect,\n onAddNewMethod,\n onValidationChange,\n title = \"Payment Method\",\n description,\n showTitle = true,\n allowNewMethod = true,\n requirePaymentMethod = true,\n loading = false,\n disabled = false,\n variant = \"default\",\n children,\n footerContent,\n}: PaymentFormProps) {\n const [selectedMethod, setSelectedMethod] = useState(selectedMethodId || \"\");\n const [errors, setErrors] = useState([]);\n\n const validatePayment = () => {\n const validationErrors: string[] = [];\n\n if (requirePaymentMethod) {\n if (existingMethods.length === 0) {\n validationErrors.push(\n \"No payment method on file. Please add a payment method to continue.\"\n );\n } else if (!selectedMethod) {\n validationErrors.push(\"Please select a payment method.\");\n }\n }\n\n setErrors(validationErrors);\n const isValid = validationErrors.length === 0;\n onValidationChange?.(isValid, validationErrors);\n\n return isValid;\n };\n\n const handleMethodSelect = (methodId: string) => {\n if (disabled) return;\n\n setSelectedMethod(methodId);\n onMethodSelect?.(methodId);\n };\n\n useEffect(() => {\n validatePayment();\n }, [selectedMethod, existingMethods, requirePaymentMethod]);\n\n useEffect(() => {\n if (selectedMethodId !== undefined) {\n setSelectedMethod(selectedMethodId);\n }\n }, [selectedMethodId]);\n\n const getCardBrandIcon = (brand?: string) => {\n // In a real implementation, you'd return appropriate brand icons\n return ;\n };\n\n const formatCardNumber = (last4?: string) => {\n return last4 ? `•••• •••• •••• ${last4}` : \"•••• •••• •••• ••••\";\n };\n\n const formatExpiry = (month?: number, year?: number) => {\n if (!month || !year) return \"\";\n return `${month.toString().padStart(2, \"0\")}/${year.toString().slice(-2)}`;\n };\n\n const containerClasses =\n variant === \"inline\"\n ? \"\"\n : variant === \"compact\"\n ? \"p-4 bg-gray-50 rounded-lg border border-gray-200\"\n : \"p-6 bg-white border border-gray-200 rounded-lg\";\n\n if (loading) {\n return (\n
\n
\n \n Loading payment methods...\n
\n
\n );\n }\n\n return (\n
\n {showTitle && (\n
\n
\n \n

{title}

\n
\n {description &&

{description}

}\n
\n )}\n\n {/* No payment methods */}\n {existingMethods.length === 0 ? (\n
\n
\n \n
\n

No Payment Method on File

\n

Add a payment method to complete your order.

\n {allowNewMethod && onAddNewMethod && (\n \n Add Payment Method\n \n )}\n
\n ) : (\n
\n {/* Existing payment methods */}\n
\n {existingMethods.map(method => (\n \n handleMethodSelect(method.id)}\n disabled={disabled}\n className=\"text-blue-600 focus:ring-blue-500 mr-4\"\n />\n\n
\n
{getCardBrandIcon(method.brand)}
\n\n
\n
\n \n {method.brand?.toUpperCase()} {formatCardNumber(method.last4)}\n \n {method.isDefault && (\n \n Default\n \n )}\n
\n\n
\n {method.name && {method.name} • }\n Expires {formatExpiry(method.expiryMonth, method.expiryYear)}\n
\n
\n\n {selectedMethod === method.id && (\n \n )}\n
\n \n ))}\n
\n\n {/* Add new payment method option */}\n {allowNewMethod && onAddNewMethod && (\n
\n \n \n Add New Payment Method\n \n
\n )}\n
\n )}\n\n {/* Custom content */}\n {children &&
{children}
}\n\n {/* Validation errors */}\n {errors.length > 0 && (\n
\n
\n \n
\n

Payment Required

\n
    \n {errors.map((error, index) => (\n
  • {error}
  • \n ))}\n
\n
\n
\n
\n )}\n\n {/* Footer content */}\n {footerContent &&
{footerContent}
}\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/PricingDisplay.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/ProductCard.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'id' is defined but never used.","line":45,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":45,"endColumn":5},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'sku' is defined but never used.","line":47,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":47,"endColumn":6}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { ReactNode } from \"react\";\nimport { CurrencyYenIcon, ArrowRightIcon } from \"@heroicons/react/24/outline\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { Button } from \"@/components/ui/button\";\n\nexport interface ProductCardProps {\n // Core product info\n id: string;\n name: string;\n sku: string;\n description?: string;\n\n // Pricing\n monthlyPrice?: number;\n oneTimePrice?: number;\n\n // Visual elements\n icon?: ReactNode;\n badge?: {\n text: string;\n variant: \"default\" | \"recommended\" | \"family\" | \"success\";\n };\n\n // Features list\n features?: string[];\n\n // Styling\n variant?: \"default\" | \"highlighted\" | \"success\";\n size?: \"compact\" | \"standard\" | \"large\";\n\n // Actions\n href?: string;\n onClick?: () => void;\n actionLabel?: string;\n disabled?: boolean;\n\n // Additional content\n children?: ReactNode;\n footer?: ReactNode;\n}\n\nexport function ProductCard({\n id,\n name,\n sku,\n description,\n monthlyPrice,\n oneTimePrice,\n icon,\n badge,\n features = [],\n variant = \"default\",\n size = \"standard\",\n href,\n onClick,\n actionLabel = \"Configure\",\n disabled = false,\n children,\n footer,\n}: ProductCardProps) {\n const sizeClasses = {\n compact: \"p-4\",\n standard: \"p-6\",\n large: \"p-8\",\n };\n\n const getBadgeClasses = (badgeVariant: string) => {\n switch (badgeVariant) {\n case \"recommended\":\n return \"bg-green-100 text-green-800 border-green-300\";\n case \"family\":\n return \"bg-blue-100 text-blue-800 border-blue-300\";\n case \"success\":\n return \"bg-emerald-100 text-emerald-800 border-emerald-300\";\n default:\n return \"bg-gray-100 text-gray-800 border-gray-300\";\n }\n };\n\n return (\n \n {/* Header with badge and icon */}\n
\n
\n {icon &&
{icon}
}\n
\n {badge && (\n \n {badge.text}\n \n )}\n
\n
\n\n {/* Pricing display */}\n {(monthlyPrice || oneTimePrice) && (\n
\n {monthlyPrice && (\n
\n \n {monthlyPrice.toLocaleString()}\n /month\n
\n )}\n {oneTimePrice && (\n
\n \n {oneTimePrice.toLocaleString()}\n one-time\n
\n )}\n
\n )}\n
\n\n {/* Product name and description */}\n
\n

{name}

\n {description &&

{description}

}\n
\n\n {/* Features list */}\n {features.length > 0 && (\n
\n

Features:

\n
    \n {features.map((feature, index) => (\n
  • \n \n {feature}\n
  • \n ))}\n
\n
\n )}\n\n {/* Custom children content */}\n {children &&
{children}
}\n\n {/* Action button */}\n
\n {href ? (\n \n ) : onClick ? (\n \n ) : null}\n
\n\n {/* Custom footer */}\n {footer &&
{footer}
}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/base/ProductComparison.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/common/FeatureCard.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon,·title,·description·}:·{·icon:·React.ReactNode;·title:·string;·description:·string·` with `⏎··icon,⏎··title,⏎··description,⏎}:·{⏎··icon:·React.ReactNode;⏎··title:·string;⏎··description:·string;⏎`","line":6,"column":30,"nodeType":null,"messageId":"replace","endLine":6,"endColumn":119,"fix":{"range":[134,223],"text":"\n icon,\n title,\n description,\n}: {\n icon: React.ReactNode;\n title: string;\n description: string;\n"}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":17,"column":1,"nodeType":null,"messageId":"delete","endLine":18,"endColumn":1,"fix":{"range":[592,593],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"\"use client\";\n\nimport React from \"react\";\nimport { AnimatedCard } from \"@/components/ui/animated-card\";\n\nexport function FeatureCard({ icon, title, description }: { icon: React.ReactNode; title: string; description: string }) {\n return (\n \n
\n
{icon}
\n
\n

{title}

\n

{description}

\n
\n );\n}\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/common/ServiceHeroCard.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·as=\"a\"·href={href}·className=\"w-full·font-semibold·rounded-2xl·relative·z-10·group\"·size=\"lg\"` with `⏎············as=\"a\"⏎············href={href}⏎············className=\"w-full·font-semibold·rounded-2xl·relative·z-10·group\"⏎············size=\"lg\"⏎··········`","line":76,"column":18,"nodeType":null,"messageId":"replace","endLine":76,"endColumn":112,"fix":{"range":[2221,2315],"text":"\n as=\"a\"\n href={href}\n className=\"w-full font-semibold rounded-2xl relative z-10 group\"\n size=\"lg\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·className={`absolute·inset-0·${colors.bg}·opacity-0·group-hover:opacity-10·transition-opacity·duration-300·pointer-events-none`}` with `⏎········className={`absolute·inset-0·${colors.bg}·opacity-0·group-hover:opacity-10·transition-opacity·duration-300·pointer-events-none`}⏎·····`","line":82,"column":11,"nodeType":null,"messageId":"replace","endLine":82,"endColumn":140,"fix":{"range":[2517,2646],"text":"\n className={`absolute inset-0 ${colors.bg} opacity-0 group-hover:opacity-10 transition-opacity duration-300 pointer-events-none`}\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":86,"column":1,"nodeType":null,"messageId":"delete","endLine":87,"endColumn":1,"fix":{"range":[2677,2678],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":3,"source":"\"use client\";\n\nimport React from \"react\";\nimport { AnimatedCard } from \"@/components/ui/animated-card\";\nimport { Button } from \"@/components/ui/button\";\nimport { ArrowRightIcon } from \"@heroicons/react/24/outline\";\n\nexport function ServiceHeroCard({\n title,\n description,\n icon,\n features,\n href,\n color,\n}: {\n title: string;\n description: string;\n icon: React.ReactNode;\n features: string[];\n href: string;\n color: \"blue\" | \"green\" | \"purple\";\n}) {\n const colorClasses = {\n blue: {\n bg: \"bg-blue-50\",\n border: \"border-blue-200\",\n iconBg: \"bg-blue-100\",\n iconText: \"text-blue-600\",\n button: \"bg-blue-600 hover:bg-blue-700\",\n hoverBorder: \"hover:border-blue-300\",\n },\n green: {\n bg: \"bg-green-50\",\n border: \"border-green-200\",\n iconBg: \"bg-green-100\",\n iconText: \"text-green-600\",\n button: \"bg-green-600 hover:bg-green-700\",\n hoverBorder: \"hover:border-green-300\",\n },\n purple: {\n bg: \"bg-purple-50\",\n border: \"border-purple-200\",\n iconBg: \"bg-purple-100\",\n iconText: \"text-purple-600\",\n button: \"bg-purple-600 hover:bg-purple-700\",\n hoverBorder: \"hover:border-purple-300\",\n },\n } as const;\n\n const colors = colorClasses[color];\n\n return (\n \n
\n
\n
\n
{icon}
\n
\n
\n

{title}

\n
\n
\n\n

{description}

\n\n
    \n {features.map((feature, index) => (\n
  • \n
    \n {feature}\n
  • \n ))}\n
\n\n
\n \n
\n
\n
\n \n );\n}\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/internet/InstallationOptions.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/internet/InternetConfigureView.tsx","messages":[{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `"`, `“`, `"`, `”`.","line":185,"column":57,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as "Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"“"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as “Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `“`."},{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as "Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"”"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as ”Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `”`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `"`, `“`, `"`, `”`.","line":185,"column":76,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan". Device subscriptions\n will be added later.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"“"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan“. Device subscriptions\n will be added later.\n "},"desc":"Replace with `“`."},{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan". Device subscriptions\n will be added later.\n "},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"”"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan”. Device subscriptions\n will be added later.\n "},"desc":"Replace with `”`."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { Button } from \"@/components/ui/button\";\nimport { ProgressSteps, StepHeader } from \"@/components/ui\";\nimport { AddonGroup } from \"@/features/catalog/components/base/AddonGroup\";\nimport { InstallationOptions } from \"@/features/catalog/components/internet/InstallationOptions\";\nimport { ServerIcon, ArrowLeftIcon, ArrowRightIcon } from \"@heroicons/react/24/outline\";\nimport type {\n InternetPlan,\n InternetAddon,\n InternetInstallation,\n} from \"@/shared/types/catalog.types\";\nimport type { AccessMode } from \"../../hooks/useConfigureParams\";\n\ntype Props = {\n plan: InternetPlan | null;\n loading: boolean;\n addons: InternetAddon[];\n installations: InternetInstallation[];\n\n mode: AccessMode | null;\n setMode: (mode: AccessMode) => void;\n installPlan: string | null;\n setInstallPlan: (type: string | null) => void;\n selectedAddonSkus: string[];\n setSelectedAddonSkus: (skus: string[]) => void;\n\n currentStep: number;\n isTransitioning: boolean;\n transitionToStep: (nextStep: number) => void;\n\n monthlyTotal: number;\n oneTimeTotal: number;\n\n onConfirm: () => void;\n};\n\nexport function InternetConfigureView({\n plan,\n loading,\n addons,\n installations,\n mode,\n setMode,\n installPlan,\n setInstallPlan,\n selectedAddonSkus,\n setSelectedAddonSkus,\n currentStep,\n isTransitioning,\n transitionToStep,\n monthlyTotal,\n oneTimeTotal,\n onConfirm,\n}: Props) {\n const handleAddonSelection = (newSelectedSkus: string[]) => setSelectedAddonSkus(newSelectedSkus);\n\n if (loading) {\n return (\n }\n title=\"Configure Internet Service\"\n description=\"Set up your internet service options\"\n >\n
\n \n
\n \n );\n }\n\n if (!plan) {\n return (\n }\n title=\"Configure Internet Service\"\n description=\"Set up your internet service options\"\n >\n
\n

Plan not found

\n \n
\n \n );\n }\n\n const steps = [\n { number: 1, title: \"Service Details\", completed: currentStep > 1 },\n { number: 2, title: \"Installation\", completed: currentStep > 2 },\n { number: 3, title: \"Add-ons\", completed: currentStep > 3 },\n { number: 4, title: \"Review Order\", completed: currentStep > 4 },\n ];\n\n return (\n } title=\"\" description=\"\">\n
\n
\n \n \n Back to Internet Plans\n \n\n

Configure {plan.name}

\n\n
\n \n {plan.tier}\n
\n \n {plan.name}\n {plan.monthlyPrice && (\n <>\n \n \n ¥{plan.monthlyPrice.toLocaleString()}/month\n \n \n )}\n
\n
\n\n \n\n
\n {currentStep === 1 && (\n \n
\n
\n
\n 1\n
\n

Service Configuration

\n
\n

Review your plan details and configuration

\n
\n\n {plan?.tier === \"Platinum\" && (\n
\n
\n
\n \n \n \n
\n
\n
\n IMPORTANT - For PLATINUM subscribers\n
\n

\n Additional fees are incurred for the PLATINUM service. Please refer to the\n information from our tech team for details.\n

\n

\n * Will appear on the invoice as \"Platinum Base Plan\". Device subscriptions\n will be added later.\n

\n
\n
\n
\n )}\n\n {plan?.tier === \"Silver\" ? (\n
\n

\n Select Your Router & ISP Configuration:\n

\n
\n setMode(\"PPPoE\")}\n className={`p-6 rounded-xl border-2 text-left transition-all duration-300 ease-in-out transform hover:scale-[1.02] hover:shadow-md ${mode === \"PPPoE\" ? \"border-orange-500 bg-orange-50 shadow-md scale-[1.02]\" : \"border-gray-200 hover:border-orange-300\"}`}\n >\n
PPPoE
\n

\n Requires a PPPoE-capable router and ISP credentials.\n

\n
\n

\n Note: Older standard, may be slower during peak times.\n

\n
\n \n\n setMode(\"IPoE-BYOR\")}\n className={`p-6 rounded-xl border-2 text-left transition-all duration-300 ease-in-out transform hover:scale-[1.02] hover:shadow-md ${mode === \"IPoE-BYOR\" ? \"border-green-500 bg-green-50 shadow-md scale-[1.02]\" : \"border-gray-200 hover:border-green-300\"}`}\n >\n
IPoE (v6plus)
\n

\n Requires a v6plus-compatible router for faster, more stable connection.\n

\n
\n

\n Recommended: Faster speeds with less congestion.{\" \"}\n \n Check compatibility →\n \n

\n
\n \n
\n\n
\n mode && transitionToStep(2)}\n disabled={!mode}\n className=\"flex items-center\"\n >\n Continue to Installation\n \n \n
\n
\n ) : (\n
\n
\n
\n \n \n \n \n Access Mode: IPoE-HGW (Pre-configured for {plan?.tier} plan)\n \n
\n
\n
\n {\n setMode(\"IPoE-BYOR\");\n transitionToStep(2);\n }}\n className=\"flex items-center\"\n >\n Continue to Installation\n \n \n
\n
\n )}\n \n )}\n\n {currentStep === 2 && mode && (\n \n
\n \n
\n\n inst.type === installPlan) || null}\n onInstallationSelect={installation => setInstallPlan(installation.type)}\n showSkus={false}\n />\n\n
\n
\n
\n \n \n \n
\n
\n

Weekend Installation

\n

\n Weekend installation is available with an additional ¥3,000 charge. Our team\n will contact you to schedule the most convenient time.\n

\n
\n
\n
\n\n
\n transitionToStep(1)}\n variant=\"outline\"\n className=\"flex items-center\"\n >\n \n Back to Service Details\n \n installPlan && transitionToStep(3)}\n disabled={!installPlan}\n className=\"flex items-center\"\n >\n Continue to Add-ons\n \n \n
\n \n )}\n\n {currentStep === 3 && installPlan && (\n \n
\n \n
\n \n
\n transitionToStep(2)}\n variant=\"outline\"\n className=\"flex items-center\"\n >\n \n Back to Installation\n \n \n
\n \n )}\n\n {currentStep === 4 && (\n \n
\n \n
\n\n
\n
\n

Order Summary

\n

Review your configuration

\n
\n\n
\n
\n
\n

{plan.name}

\n

Internet Service

\n
\n
\n

\n ¥{plan.monthlyPrice?.toLocaleString()}\n

\n

per month

\n
\n
\n
\n\n
\n

Configuration

\n
\n
\n Access Mode:\n {mode || \"Not selected\"}\n
\n
\n
\n\n {selectedAddonSkus.length > 0 && (\n
\n

Add-ons

\n
\n {selectedAddonSkus.map(addonSku => {\n const addon = addons.find(a => a.sku === addonSku);\n return (\n
\n {addon?.name || addonSku}\n \n ¥\n {(\n addon?.monthlyPrice ||\n addon?.activationPrice ||\n 0\n ).toLocaleString()}\n \n /{addon?.monthlyPrice ? \"mo\" : \"once\"}\n \n \n
\n );\n })}\n
\n
\n )}\n\n {(() => {\n const installation = installations.find(i => i.type === installPlan);\n return installation && installation.price && installation.price > 0 ? (\n
\n

Installation

\n
\n {installation.name}\n \n ¥{installation.price.toLocaleString()}\n \n /{installation.billingCycle === \"Monthly\" ? \"mo\" : \"once\"}\n \n \n
\n
\n ) : null;\n })()}\n\n
\n
\n
\n Monthly Total\n ¥{monthlyTotal.toLocaleString()}\n
\n {oneTimeTotal > 0 && (\n
\n One-time Total\n \n ¥{oneTimeTotal.toLocaleString()}\n \n
\n )}\n
\n
\n
\n\n
\n transitionToStep(3)}\n variant=\"outline\"\n size=\"lg\"\n className=\"px-8 py-4 text-lg\"\n >\n \n Back to Add-ons\n \n \n
\n \n )}\n
\n
\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/sim/ActivationForm.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/sim/MnpForm.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/sim/SimConfigureView.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/sim/SimPlanCard.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `Family` with `⏎················Family⏎··············`","line":20,"column":104,"nodeType":null,"messageId":"replace","endLine":20,"endColumn":110,"fix":{"range":[1036,1042],"text":"\n Family\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `{plan.monthlyPrice?.toLocaleString()}` with `⏎············{plan.monthlyPrice?.toLocaleString()}⏎··········`","line":28,"column":61,"nodeType":null,"messageId":"replace","endLine":28,"endColumn":98,"fix":{"range":[1315,1352],"text":"\n {plan.monthlyPrice?.toLocaleString()}\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `Discounted·price` with `(⏎··········Discounted·price⏎········)`","line":31,"column":22,"nodeType":null,"messageId":"replace","endLine":31,"endColumn":101,"fix":{"range":[1460,1539],"text":"(\n
Discounted price
\n )"}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":42,"column":1,"nodeType":null,"messageId":"delete","endLine":43,"endColumn":1,"fix":{"range":[1838,1839],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":4,"fixableErrorCount":0,"fixableWarningCount":4,"source":"\"use client\";\n\nimport { DevicePhoneMobileIcon, UsersIcon, CurrencyYenIcon } from \"@heroicons/react/24/outline\";\nimport { AnimatedCard } from \"@/components/ui/animated-card\";\nimport { Button } from \"@/components/ui/button\";\nimport type { SimPlan } from \"@/shared/types/catalog.types\";\n\nexport function SimPlanCard({ plan, isFamily }: { plan: SimPlan; isFamily?: boolean }) {\n return (\n \n
\n
\n
\n \n {plan.dataSize}\n
\n {isFamily && (\n
\n \n Family\n
\n )}\n
\n
\n
\n
\n \n {plan.monthlyPrice?.toLocaleString()}\n /month\n
\n {isFamily &&
Discounted price
}\n
\n
\n

{plan.description}

\n
\n \n
\n );\n}\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/sim/SimPlanTypeSection.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `You·qualify!` with `⏎··············You·qualify!⏎············`","line":44,"column":90,"nodeType":null,"messageId":"replace","endLine":44,"endColumn":102,"fix":{"range":[1502,1514],"text":"\n You qualify!\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":56,"column":1,"nodeType":null,"messageId":"delete","endLine":57,"endColumn":1,"fix":{"range":[1832,1833],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":2,"source":"\"use client\";\n\nimport React from \"react\";\nimport { UsersIcon } from \"@heroicons/react/24/outline\";\nimport type { SimPlan } from \"@/shared/types/catalog.types\";\nimport { SimPlanCard } from \"./SimPlanCard\";\n\nexport function SimPlanTypeSection({\n title,\n description,\n icon,\n plans,\n showFamilyDiscount,\n}: {\n title: string;\n description: string;\n icon: React.ReactNode;\n plans: SimPlan[];\n showFamilyDiscount: boolean;\n}) {\n if (plans.length === 0) return null;\n const regularPlans = plans.filter(p => !p.hasFamilyDiscount);\n const familyPlans = plans.filter(p => p.hasFamilyDiscount);\n\n return (\n
\n
\n {icon}\n
\n

{title}

\n

{description}

\n
\n
\n
\n {regularPlans.map(plan => (\n \n ))}\n
\n {showFamilyDiscount && familyPlans.length > 0 && (\n <>\n
\n \n

Family Discount Options

\n You qualify!\n
\n
\n {familyPlans.map(plan => (\n \n ))}\n
\n \n )}\n
\n );\n}\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/components/sim/SimTypeSelector.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/containers/CatalogHome.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·Squares2X2Icon,·ServerIcon,·DevicePhoneMobileIcon,·ShieldCheckIcon,·WifiIcon,·GlobeAltIcon·` with `⏎··Squares2X2Icon,⏎··ServerIcon,⏎··DevicePhoneMobileIcon,⏎··ShieldCheckIcon,⏎··WifiIcon,⏎··GlobeAltIcon,⏎`","line":5,"column":9,"nodeType":null,"messageId":"replace","endLine":5,"endColumn":101,"fix":{"range":[111,203],"text":"\n Squares2X2Icon,\n ServerIcon,\n DevicePhoneMobileIcon,\n ShieldCheckIcon,\n WifiIcon,\n GlobeAltIcon,\n"}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `Connectivity·Solution` with `⏎··············Connectivity·Solution⏎············`","line":21,"column":106,"nodeType":null,"messageId":"replace","endLine":21,"endColumn":127,"fix":{"range":[1066,1087],"text":"\n Connectivity Solution\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `\"Up·to·10Gbps·speeds\",·\"Fiber·optic·technology\",·\"Multiple·access·modes\",·\"Professional·installation\"` with `⏎··············\"Up·to·10Gbps·speeds\",⏎··············\"Fiber·optic·technology\",⏎··············\"Multiple·access·modes\",⏎··············\"Professional·installation\",⏎············`","line":33,"column":24,"nodeType":null,"messageId":"replace","endLine":33,"endColumn":125,"fix":{"range":[1615,1716],"text":"\n \"Up to 10Gbps speeds\",\n \"Fiber optic technology\",\n \"Multiple access modes\",\n \"Professional installation\",\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `\"Physical·SIM·&·eSIM\",·\"Data·+·SMS/Voice·plans\",·\"Family·discounts\",·\"Multiple·data·options\"` with `⏎··············\"Physical·SIM·&·eSIM\",⏎··············\"Data·+·SMS/Voice·plans\",⏎··············\"Family·discounts\",⏎··············\"Multiple·data·options\",⏎············`","line":41,"column":24,"nodeType":null,"messageId":"replace","endLine":41,"endColumn":116,"fix":{"range":[2036,2128],"text":"\n \"Physical SIM & eSIM\",\n \"Data + SMS/Voice plans\",\n \"Family discounts\",\n \"Multiple data options\",\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `\"Secure·encryption\",·\"Multiple·locations\",·\"Business·&·personal\",·\"24/7·connectivity\"` with `⏎··············\"Secure·encryption\",⏎··············\"Multiple·locations\",⏎··············\"Business·&·personal\",⏎··············\"24/7·connectivity\",⏎············`","line":49,"column":24,"nodeType":null,"messageId":"replace","endLine":49,"endColumn":109,"fix":{"range":[2433,2518],"text":"\n \"Secure encryption\",\n \"Multiple locations\",\n \"Business & personal\",\n \"24/7 connectivity\",\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon={}·title=\"Location-Based·Plans\"·description=\"Internet·plans·tailored·to·your·house·type·and·infrastructure\"` with `⏎··············icon={}⏎··············title=\"Location-Based·Plans\"⏎··············description=\"Internet·plans·tailored·to·your·house·type·and·infrastructure\"⏎···········`","line":63,"column":25,"nodeType":null,"messageId":"replace","endLine":63,"endColumn":186,"fix":{"range":[3158,3319],"text":"\n icon={}\n title=\"Location-Based Plans\"\n description=\"Internet plans tailored to your house type and infrastructure\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon={}·title=\"Seamless·Integration\"·description=\"Manage·all·services·from·a·single·account\"` with `⏎··············icon={}⏎··············title=\"Seamless·Integration\"⏎··············description=\"Manage·all·services·from·a·single·account\"⏎···········`","line":64,"column":25,"nodeType":null,"messageId":"replace","endLine":64,"endColumn":172,"fix":{"range":[3347,3494],"text":"\n icon={}\n title=\"Seamless Integration\"\n description=\"Manage all services from a single account\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":73,"column":1,"nodeType":null,"messageId":"delete","endLine":74,"endColumn":1,"fix":{"range":[3606,3607],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":8,"fixableErrorCount":0,"fixableWarningCount":8,"source":"\"use client\";\n\nimport React from \"react\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { Squares2X2Icon, ServerIcon, DevicePhoneMobileIcon, ShieldCheckIcon, WifiIcon, GlobeAltIcon } from \"@heroicons/react/24/outline\";\nimport { ServiceHeroCard } from \"@/features/catalog/components/common/ServiceHeroCard\";\nimport { FeatureCard } from \"@/features/catalog/components/common/FeatureCard\";\n\nexport function CatalogHomeContainer() {\n return (\n } title=\"\" description=\"\">\n
\n
\n
\n \n Services Catalog\n
\n

\n Choose Your Perfect\n
\n Connectivity Solution\n

\n

\n Discover high-speed internet, mobile data/voice options, and secure VPN services.\n

\n
\n\n
\n }\n features={[\"Up to 10Gbps speeds\", \"Fiber optic technology\", \"Multiple access modes\", \"Professional installation\"]}\n href=\"/catalog/internet\"\n color=\"blue\"\n />\n }\n features={[\"Physical SIM & eSIM\", \"Data + SMS/Voice plans\", \"Family discounts\", \"Multiple data options\"]}\n href=\"/catalog/sim\"\n color=\"green\"\n />\n }\n features={[\"Secure encryption\", \"Multiple locations\", \"Business & personal\", \"24/7 connectivity\"]}\n href=\"/catalog/vpn\"\n color=\"purple\"\n />\n
\n\n
\n
\n

Why Choose Our Services?

\n

\n Personalized recommendations based on your location and account eligibility.\n

\n
\n
\n } title=\"Location-Based Plans\" description=\"Internet plans tailored to your house type and infrastructure\" />\n } title=\"Seamless Integration\" description=\"Manage all services from a single account\" />\n
\n
\n
\n
\n );\n}\n\nexport default CatalogHomeContainer;\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/containers/InternetConfigure.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/containers/InternetPlans.tsx","messages":[{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"The 'plans' logical expression could make the dependencies of useEffect Hook (at line 30) change on every render. To fix this, wrap the initialization of 'plans' in its own useMemo() Hook.","line":22,"column":9,"nodeType":"VariableDeclarator","endLine":22,"endColumn":34},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":148,"column":24,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[5836,5941],"text":"\n We couldn't find any internet plans available for your location at this time.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[5836,5941],"text":"\n We couldn‘t find any internet plans available for your location at this time.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[5836,5941],"text":"\n We couldn't find any internet plans available for your location at this time.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[5836,5941],"text":"\n We couldn’t find any internet plans available for your location at this time.\n "},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":229,"column":90,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet's Hikari\n Next - "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet‘s Hikari\n Next - "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet's Hikari\n Next - "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet’s Hikari\n Next - "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useState, useEffect } from \"react\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport {\n WifiIcon,\n ServerIcon,\n CurrencyYenIcon,\n ArrowLeftIcon,\n ArrowRightIcon,\n HomeIcon,\n BuildingOfficeIcon,\n} from \"@heroicons/react/24/outline\";\nimport { useInternetCatalog } from \"@/features/catalog/hooks\";\nimport { InternetPlan, InternetInstallation } from \"@/shared/types/catalog.types\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function InternetPlansContainer() {\n const { data, isLoading, error } = useInternetCatalog();\n const plans = data?.plans || [];\n const installations = data?.installations || [];\n const [eligibility, setEligibility] = useState(\"\");\n\n useEffect(() => {\n if (plans.length > 0) {\n setEligibility(plans[0].offeringType || \"Home 1G\");\n }\n }, [plans]);\n\n const getEligibilityIcon = (offeringType: string) => {\n if (offeringType.toLowerCase().includes(\"home\")) return ;\n if (offeringType.toLowerCase().includes(\"apartment\"))\n return ;\n return ;\n };\n\n const getEligibilityColor = (offeringType: string) => {\n if (offeringType.toLowerCase().includes(\"home\"))\n return \"text-blue-600 bg-blue-50 border-blue-200\";\n if (offeringType.toLowerCase().includes(\"apartment\"))\n return \"text-green-600 bg-green-50 border-green-200\";\n return \"text-gray-600 bg-gray-50 border-gray-200\";\n };\n\n if (isLoading) {\n return (\n }\n >\n
\n \n
\n \n );\n }\n\n if (error) {\n const errorMessage = error instanceof Error ? error.message : \"An unexpected error occurred\";\n return (\n }\n >\n
\n
Failed to load plans
\n
{errorMessage}
\n \n
\n \n );\n }\n\n return (\n }\n >\n
\n
\n \n
\n\n
\n

Choose Your Internet Plan

\n\n {eligibility && (\n
\n \n {getEligibilityIcon(eligibility)}\n Available for: {eligibility}\n
\n

\n Plans shown are tailored to your house type and local infrastructure\n

\n
\n )}\n
\n\n {plans.length > 0 ? (\n <>\n
\n {plans.map(plan => (\n \n ))}\n
\n\n
\n

Important Notes:

\n
    \n
  • \n Theoretical internet speed is the\n same for all three packages\n
  • \n
  • \n One-time fee (¥22,800) can be paid\n upfront or in 12- or 24-month installments\n
  • \n
  • \n Home phone line (Hikari Denwa) can be\n added to GOLD or PLATINUM plans (¥450/month + ¥1,000-3,000 one-time)\n
  • \n
  • \n In-home technical assistance\n available (¥15,000 onsite visiting fee)\n
  • \n
\n
\n \n ) : (\n
\n \n

No Plans Available

\n

\n We couldn't find any internet plans available for your location at this time.\n

\n \n
\n )}\n \n \n );\n}\n\nfunction InternetPlanCard({\n plan,\n installations,\n}: {\n plan: InternetPlan;\n installations: InternetInstallation[];\n}) {\n const isGold = plan.tier === \"Gold\";\n const isPlatinum = plan.tier === \"Platinum\";\n const isSilver = plan.tier === \"Silver\";\n\n const cardVariant = \"default\";\n\n const getBorderClass = () => {\n if (isGold) return \"border-2 border-yellow-400 shadow-lg hover:shadow-xl\";\n if (isPlatinum) return \"border-2 border-indigo-400 shadow-lg hover:shadow-xl\";\n if (isSilver) return \"border-2 border-gray-300 shadow-lg hover:shadow-xl\";\n return \"border border-gray-200 shadow-lg hover:shadow-xl\";\n };\n\n return (\n \n
\n
\n
\n \n {plan.tier}\n \n {isGold && (\n \n Recommended\n \n )}\n
\n {plan.monthlyPrice && (\n
\n
\n \n {plan.monthlyPrice.toLocaleString()}\n \n per month\n \n
\n
\n )}\n
\n\n

{plan.name}

\n

{plan.tierDescription || plan.description}

\n\n
\n

Your Plan Includes:

\n
    \n {plan.features && plan.features.length > 0 ? (\n plan.features.map((feature, index) => (\n
  • \n \n {feature}\n
  • \n ))\n ) : (\n <>\n
  • \n 1 NTT Optical Fiber (Flet's Hikari\n Next - {plan.offeringType?.includes(\"Apartment\") ? \"Mansion\" : \"Home\"}{\" \"}\n {plan.offeringType?.includes(\"10G\")\n ? \"10Gbps\"\n : plan.offeringType?.includes(\"100M\")\n ? \"100Mbps\"\n : \"1Gbps\"}\n ) Installation + Monthly\n
  • \n
  • \n \n Monthly: ¥{plan.monthlyPrice?.toLocaleString()}\n {installations.length > 0 && (\n \n (+ installation from ¥\n {Math.min(...installations.map(i => i.price || 0)).toLocaleString()})\n \n )}\n
  • \n \n )}\n
\n
\n\n \n Configure Plan\n \n \n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/containers/SimConfigure.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/containers/VpnPlans.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'VpnPlan' is defined but never used.","line":6,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":6,"endColumn":17},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'VpnActivationFee' is defined but never used.","line":6,"column":19,"nodeType":null,"messageId":"unusedVar","endLine":6,"endColumn":35},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":122,"column":24,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[4876,4958],"text":"\n We couldn't find any VPN plans available at this time.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[4876,4958],"text":"\n We couldn‘t find any VPN plans available at this time.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[4876,4958],"text":"\n We couldn't find any VPN plans available at this time.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[4876,4958],"text":"\n We couldn’t find any VPN plans available at this time.\n "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport { ShieldCheckIcon, CurrencyYenIcon, ArrowLeftIcon } from \"@heroicons/react/24/outline\";\nimport { useVpnCatalog } from \"@/features/catalog/hooks\";\nimport { VpnPlan, VpnActivationFee } from \"@/shared/types/catalog.types\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function VpnPlansContainer() {\n const { data, isLoading, error } = useVpnCatalog();\n const vpnPlans = data?.plans || [];\n const activationFees = data?.activationFees || [];\n\n if (isLoading) {\n return (\n }\n >\n
\n \n
\n \n );\n }\n\n if (error) {\n const errorMessage = error instanceof Error ? error.message : \"An unexpected error occurred\";\n return (\n }\n >\n
\n
Failed to load VPN plans
\n
{errorMessage}
\n \n
\n \n );\n }\n\n return (\n }\n >\n
\n
\n \n
\n\n
\n

\n SonixNet VPN Rental Router Service\n

\n

\n Fast and secure VPN connection to San Francisco or London for accessing geo-restricted\n content.\n

\n
\n\n {vpnPlans.length > 0 ? (\n
\n

Available Plans

\n

(One region per router)

\n\n
\n {vpnPlans.map(plan => (\n \n
\n

{plan.name}

\n
\n
\n
\n \n \n {plan.monthlyPrice?.toLocaleString()}\n \n /month\n
\n
\n \n Configure Plan\n \n \n ))}\n
\n\n {activationFees.length > 0 && (\n
\n

\n A one-time activation fee of 3000 JPY is incurred seprarately for each rental\n unit. Tax (10%) not included.\n

\n
\n )}\n
\n ) : (\n
\n \n

No VPN Plans Available

\n

\n We couldn't find any VPN plans available at this time.\n

\n \n
\n )}\n\n
\n

How It Works

\n
\n

\n SonixNet VPN is the easiest way to access video streaming services from overseas on\n your network media players such as an Apple TV, Roku, or Amazon Fire.\n

\n

\n A configured Wi-Fi router is provided for rental (no purchase required, no hidden\n fees). All you will need to do is to plug the VPN router into your existing internet\n connection.\n

\n

\n Then you can connect your network media players to the VPN Wi-Fi network, to connect\n to the VPN server.\n

\n

\n For daily Internet usage that does not require a VPN, we recommend connecting to your\n regular home Wi-Fi.\n

\n
\n
\n\n
\n

Important Disclaimer

\n

\n *1: Content subscriptions are NOT included in the SonixNet VPN package. Our VPN service\n will establish a network connection that virtually locates you in the designated server\n location, then you will sign up for the streaming services of your choice. Not all\n services/websites can be unblocked. Assist Solutions does not guarantee or bear any\n responsibility over the unblocking of any websites or the quality of the\n streaming/browsing.\n

\n
\n
\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/hooks/useCatalog.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'CatalogProduct' is defined but never used.","line":8,"column":15,"nodeType":null,"messageId":"unusedVar","endLine":8,"endColumn":29},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":64,"column":7,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":64,"endColumn":63,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[1721,1721],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[1721,1721],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":65,"column":7,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":65,"endColumn":70,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[1784,1784],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[1784,1784],"text":"await "},"desc":"Add await operator."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Catalog Hooks\n * React hooks for catalog functionality\n */\n\nimport { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { catalogService } from \"../services\";\nimport type { CatalogProduct, CatalogFilters, ProductConfiguration, OrderSummary } from \"../types\";\n\n/**\n * Hook to fetch all products with optional filtering\n */\nexport function useProducts(filters?: CatalogFilters) {\n return useQuery({\n queryKey: [\"catalog\", \"products\", filters],\n queryFn: () => catalogService.getProducts(filters),\n staleTime: 5 * 60 * 1000, // 5 minutes\n });\n}\n\n/**\n * Hook to fetch a specific product\n */\nexport function useProduct(id: string) {\n return useQuery({\n queryKey: [\"catalog\", \"product\", id],\n queryFn: () => catalogService.getProduct(id),\n enabled: !!id,\n staleTime: 5 * 60 * 1000, // 5 minutes\n });\n}\n\n/**\n * Hook to fetch products by category\n */\nexport function useProductsByCategory(category: \"internet\" | \"sim\" | \"vpn\") {\n return useQuery({\n queryKey: [\"catalog\", \"products\", \"category\", category],\n queryFn: () => catalogService.getProductsByCategory(category),\n staleTime: 5 * 60 * 1000, // 5 minutes\n });\n}\n\n/**\n * Hook to calculate order summary\n */\nexport function useCalculateOrder() {\n return useMutation({\n mutationFn: (configuration: ProductConfiguration) =>\n catalogService.calculateOrderSummary(configuration),\n });\n}\n\n/**\n * Hook to submit an order\n */\nexport function useSubmitOrder() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: (orderSummary: OrderSummary) => catalogService.submitOrder(orderSummary),\n onSuccess: () => {\n // Invalidate relevant queries after successful order\n queryClient.invalidateQueries({ queryKey: [\"orders\"] });\n queryClient.invalidateQueries({ queryKey: [\"subscriptions\"] });\n },\n });\n}\n\n/**\n * Internet catalog composite hook\n * Fetches plans and installations together\n */\nexport function useInternetCatalog() {\n return useQuery({\n queryKey: [\"catalog\", \"internet\", \"all\"],\n queryFn: async () => {\n const [plans, installations, addons] = await Promise.all([\n catalogService.getInternetPlans(),\n catalogService.getInternetInstallations(),\n catalogService.getInternetAddons(),\n ]);\n return { plans, installations, addons } as const;\n },\n staleTime: 5 * 60 * 1000,\n });\n}\n\n/**\n * SIM catalog composite hook\n * Fetches plans, activation fees, and addons together\n */\nexport function useSimCatalog() {\n return useQuery({\n queryKey: [\"catalog\", \"sim\", \"all\"],\n queryFn: async () => {\n const [plans, activationFees, addons] = await Promise.all([\n catalogService.getSimPlans(),\n catalogService.getSimActivationFees(),\n catalogService.getSimAddons(),\n ]);\n return { plans, activationFees, addons } as const;\n },\n staleTime: 5 * 60 * 1000,\n });\n}\n\n/**\n * VPN catalog hook\n * Fetches VPN plans and activation fees\n */\nexport function useVpnCatalog() {\n return useQuery({\n queryKey: [\"catalog\", \"vpn\", \"all\"],\n queryFn: async () => {\n const [plans, activationFees] = await Promise.all([\n catalogService.getVpnPlans(),\n catalogService.getVpnActivationFees(),\n ]);\n return { plans, activationFees } as const;\n },\n staleTime: 5 * 60 * 1000,\n });\n}\n\n/**\n * Lookup helpers by SKU\n */\nexport function useInternetPlan(sku?: string) {\n const { data, ...rest } = useInternetCatalog();\n const plan = (data?.plans || []).find(p => p.sku === sku);\n return { plan, ...rest } as const;\n}\n\nexport function useSimPlan(sku?: string) {\n const { data, ...rest } = useSimCatalog();\n const plan = (data?.plans || []).find(p => p.sku === sku);\n return { plan, ...rest } as const;\n}\n\nexport function useVpnPlan(sku?: string) {\n const { data, ...rest } = useVpnCatalog();\n const plan = (data?.plans || []).find(p => p.sku === sku);\n return { plan, ...rest } as const;\n}\n\n/**\n * Addon/installation lookup helpers by SKU\n */\nexport function useInternetInstallation(sku?: string) {\n const { data, ...rest } = useInternetCatalog();\n const installation = (data?.installations || []).find(i => i.sku === sku);\n return { installation, ...rest } as const;\n}\n\nexport function useInternetAddon(sku?: string) {\n const { data, ...rest } = useInternetCatalog();\n const addon = (data?.addons || []).find(a => a.sku === sku);\n return { addon, ...rest } as const;\n}\n\nexport function useSimAddon(sku?: string) {\n const { data, ...rest } = useSimCatalog();\n const addon = (data?.addons || []).find(a => a.sku === sku);\n return { addon, ...rest } as const;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/hooks/useConfigureParams.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/hooks/useInternetConfigure.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/hooks/useSimConfigure.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/services/catalog.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/services/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/types/catalog.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/utils/catalog.utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/catalog/utils/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/checkout/containers/CheckoutContainer.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/checkout/hooks/useCheckout.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'InternetPlan' is defined but never used.","line":12,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":12,"endColumn":15},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'InternetAddon' is defined but never used.","line":13,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":13,"endColumn":16},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'InternetInstallation' is defined but never used.","line":14,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":14,"endColumn":23},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'SimPlan' is defined but never used.","line":15,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":15,"endColumn":10},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'SimAddon' is defined but never used.","line":16,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":16,"endColumn":11},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'SimActivationFee' is defined but never used.","line":17,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":17,"endColumn":19}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":6,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\nimport { catalogService } from \"@/features/catalog/services/catalog.service\";\nimport { ordersService } from \"@/features/orders/services/orders.service\";\nimport { usePaymentMethods } from \"@/features/billing/hooks/useBilling\";\nimport { usePaymentRefresh } from \"@/features/billing/hooks/usePaymentRefresh\";\nimport type {\n CheckoutState,\n OrderItem,\n InternetPlan,\n InternetAddon,\n InternetInstallation,\n SimPlan,\n SimAddon,\n SimActivationFee,\n} from \"@/shared/types/catalog.types\";\nimport {\n buildInternetOrderItems,\n buildSimOrderItems,\n calculateTotals,\n buildOrderSKUs,\n} from \"@/shared/types/catalog.types\";\n\nexport interface Address {\n street: string | null;\n streetLine2: string | null;\n city: string | null;\n state: string | null;\n postalCode: string | null;\n country: string | null;\n}\n\nexport function useCheckout() {\n const params = useSearchParams();\n const router = useRouter();\n\n const [submitting, setSubmitting] = useState(false);\n const [addressConfirmed, setAddressConfirmed] = useState(false);\n const [confirmedAddress, setConfirmedAddress] = useState
(null);\n\n const [checkoutState, setCheckoutState] = useState({\n loading: true,\n error: null,\n orderItems: [],\n totals: { monthlyTotal: 0, oneTimeTotal: 0 },\n });\n\n const {\n data: paymentMethods,\n isLoading: paymentMethodsLoading,\n error: paymentMethodsError,\n refetch: refetchPaymentMethods,\n } = usePaymentMethods();\n\n const paymentRefresh = usePaymentRefresh({\n refetch: refetchPaymentMethods,\n hasMethods: (data?: { totalCount?: number }) => !!data && (data.totalCount || 0) > 0,\n attachFocusListeners: true,\n });\n\n const orderType = useMemo(() => {\n const type = params.get(\"type\") || \"internet\";\n switch (type.toLowerCase()) {\n case \"sim\":\n return \"SIM\" as const;\n case \"internet\":\n return \"Internet\" as const;\n case \"vpn\":\n return \"VPN\" as const;\n default:\n return \"Other\" as const;\n }\n }, [params]);\n\n const selections = useMemo(() => {\n const obj: Record = {};\n params.forEach((v, k) => {\n if (k !== \"type\") obj[k] = v;\n });\n return obj;\n }, [params]);\n\n useEffect(() => {\n let mounted = true;\n void (async () => {\n try {\n setCheckoutState(prev => ({ ...prev, loading: true, error: null }));\n\n if (!selections.plan) {\n throw new Error(\"No plan selected. Please go back and select a plan.\");\n }\n\n let orderItems: OrderItem[] = [];\n\n if (orderType === \"Internet\") {\n const [plans, addons, installations] = await Promise.all([\n catalogService.getInternetPlans(),\n catalogService.getInternetAddons(),\n catalogService.getInternetInstallations(),\n ]);\n\n const plan = plans.find(p => p.sku === selections.plan);\n if (!plan) {\n throw new Error(\n `Internet plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`\n );\n }\n\n const addonSkus: string[] = [];\n const urlParams = new URLSearchParams(window.location.search);\n urlParams.getAll(\"addonSku\").forEach(sku => {\n if (sku && !addonSkus.includes(sku)) addonSkus.push(sku);\n });\n\n orderItems = buildInternetOrderItems(plan, addons, installations, {\n installationSku: selections.installationSku,\n addonSkus: addonSkus.length > 0 ? addonSkus : undefined,\n });\n } else if (orderType === \"SIM\") {\n const [plans, activationFees, addons] = await Promise.all([\n catalogService.getSimPlans(),\n catalogService.getSimActivationFees(),\n catalogService.getSimAddons(),\n ]);\n\n const plan = plans.find(p => p.sku === selections.plan);\n if (!plan) {\n throw new Error(\n `SIM plan not found for SKU: ${selections.plan}. Please go back and select a valid plan.`\n );\n }\n\n const addonSkus: string[] = [];\n if (selections.addonSku) addonSkus.push(selections.addonSku);\n const urlParams = new URLSearchParams(window.location.search);\n urlParams.getAll(\"addonSku\").forEach(sku => {\n if (sku && !addonSkus.includes(sku)) addonSkus.push(sku);\n });\n\n orderItems = buildSimOrderItems(plan, activationFees, addons, {\n addonSkus: addonSkus.length > 0 ? addonSkus : undefined,\n });\n }\n\n if (mounted) {\n const totals = calculateTotals(orderItems);\n setCheckoutState(prev => ({ ...prev, loading: false, orderItems, totals }));\n }\n } catch (error) {\n if (mounted) {\n setCheckoutState(prev => ({\n ...prev,\n loading: false,\n error: error instanceof Error ? error.message : \"Failed to load checkout data\",\n }));\n }\n }\n })();\n return () => {\n mounted = false;\n };\n }, [orderType, selections]);\n\n const handleSubmitOrder = useCallback(async () => {\n try {\n setSubmitting(true);\n const skus = buildOrderSKUs(checkoutState.orderItems);\n if (!skus || skus.length === 0) {\n throw new Error(\"No products selected for order. Please go back and select products.\");\n }\n\n const configurations: Record = {};\n if (selections.accessMode) configurations.accessMode = selections.accessMode;\n if (selections.simType) configurations.simType = selections.simType;\n if (selections.eid) configurations.eid = selections.eid;\n if (selections.activationType) configurations.activationType = selections.activationType;\n if (selections.scheduledAt) configurations.scheduledAt = selections.scheduledAt;\n if (selections.isMnp) configurations.isMnp = selections.isMnp;\n if (selections.reservationNumber) configurations.mnpNumber = selections.reservationNumber;\n if (selections.expiryDate) configurations.mnpExpiry = selections.expiryDate;\n if (selections.phoneNumber) configurations.mnpPhone = selections.phoneNumber;\n if (selections.mvnoAccountNumber)\n configurations.mvnoAccountNumber = selections.mvnoAccountNumber;\n if (selections.portingLastName) configurations.portingLastName = selections.portingLastName;\n if (selections.portingFirstName)\n configurations.portingFirstName = selections.portingFirstName;\n if (selections.portingLastNameKatakana)\n configurations.portingLastNameKatakana = selections.portingLastNameKatakana;\n if (selections.portingFirstNameKatakana)\n configurations.portingFirstNameKatakana = selections.portingFirstNameKatakana;\n if (selections.portingGender) configurations.portingGender = selections.portingGender;\n if (selections.portingDateOfBirth)\n configurations.portingDateOfBirth = selections.portingDateOfBirth;\n\n if (confirmedAddress) configurations.address = confirmedAddress;\n\n const orderData = {\n orderType,\n skus,\n ...(Object.keys(configurations).length > 0 && { configurations }),\n };\n\n if (orderType === \"SIM\") {\n if (!selections.eid && selections.simType === \"eSIM\") {\n throw new Error(\n \"EID is required for eSIM activation. Please go back and provide your EID.\"\n );\n }\n if (!selections.phoneNumber && !selections.mnpPhone) {\n throw new Error(\n \"Phone number is required for SIM activation. Please go back and provide a phone number.\"\n );\n }\n }\n\n const response = await ordersService.createOrder<{ sfOrderId: string }>(orderData);\n router.push(`/orders/${response.sfOrderId}?status=success`);\n } catch (error) {\n let errorMessage = \"Order submission failed\";\n if (error instanceof Error) errorMessage = error.message;\n setCheckoutState(prev => ({ ...prev, error: errorMessage }));\n } finally {\n setSubmitting(false);\n }\n }, [checkoutState.orderItems, confirmedAddress, orderType, selections, router]);\n\n const confirmAddress = useCallback((address?: Address) => {\n setAddressConfirmed(true);\n setConfirmedAddress(address || null);\n }, []);\n\n const markAddressIncomplete = useCallback(() => {\n setAddressConfirmed(false);\n setConfirmedAddress(null);\n }, []);\n\n const navigateBackToConfigure = useCallback(() => {\n const urlParams = new URLSearchParams(params.toString());\n const reviewStep = orderType === \"Internet\" ? \"4\" : \"5\";\n urlParams.set(\"step\", reviewStep);\n const configureUrl =\n orderType === \"Internet\"\n ? `/catalog/internet/configure?${urlParams.toString()}`\n : `/catalog/sim/configure?${urlParams.toString()}`;\n router.push(configureUrl);\n }, [orderType, params, router]);\n\n return {\n checkoutState,\n submitting,\n orderType,\n addressConfirmed,\n paymentMethods,\n paymentMethodsLoading,\n paymentMethodsError,\n paymentRefresh,\n confirmAddress,\n markAddressIncomplete,\n handleSubmitOrder,\n navigateBackToConfigure,\n } as const;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/AccountStatusCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/ActivityFeed.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/DashboardActivityItem.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'id' is defined but never used.","line":38,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":38,"endColumn":5}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport {\n DocumentTextIcon,\n CheckCircleIcon,\n ServerIcon,\n ChatBubbleLeftRightIcon,\n ExclamationTriangleIcon,\n} from \"@heroicons/react/24/outline\";\nimport { format } from \"date-fns\";\nimport { cn } from \"@/lib/utils\";\nimport { getActivityIconGradient, formatActivityDate } from \"../utils/dashboard.utils\";\nimport type { Activity } from \"@customer-portal/shared\";\n\nexport interface DashboardActivityItemProps {\n id: string | number;\n type: Activity[\"type\"];\n title: string;\n description: string;\n date: string;\n onClick?: () => void;\n className?: string;\n showRelativeTime?: boolean;\n}\n\nconst ACTIVITY_ICONS: Record<\n Activity[\"type\"],\n React.ComponentType>\n> = {\n invoice_created: DocumentTextIcon,\n invoice_paid: CheckCircleIcon,\n service_activated: ServerIcon,\n case_created: ChatBubbleLeftRightIcon,\n case_closed: CheckCircleIcon,\n};\n\nexport function DashboardActivityItem({\n id,\n type,\n title,\n description,\n date,\n onClick,\n className,\n showRelativeTime = true,\n}: DashboardActivityItemProps) {\n const Icon = ACTIVITY_ICONS[type] || ExclamationTriangleIcon;\n const gradient = getActivityIconGradient(type);\n\n const formattedDate = showRelativeTime\n ? formatActivityDate(date)\n : format(new Date(date), \"MMM d, yyyy · h:mm a\");\n\n const Wrapper = onClick ? \"button\" : \"div\";\n\n return (\n \n
\n \n \n
\n \n\n
\n \n {title}\n

\n\n {description &&

{description}

}\n\n

\n \n

\n
\n\n {onClick && (\n
\n \n \n \n
\n )}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/QuickAction.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/StatCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/hooks/useDashboardSummary.ts","messages":[{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":30,"column":15,"nodeType":"TSAsExpression","messageId":"object","endLine":33,"endColumn":28},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":41,"column":17,"nodeType":"TSAsExpression","messageId":"object","endLine":45,"endColumn":30},{"ruleId":"@typescript-eslint/only-throw-error","severity":2,"message":"Expected an error object to be thrown.","line":48,"column":15,"nodeType":"TSAsExpression","messageId":"object","endLine":52,"endColumn":28}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Dashboard Summary Hook\n * Provides dashboard data with proper error handling, caching, and loading states\n */\n\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { dashboardService } from \"../services/dashboard.service\";\nimport type { DashboardSummary, DashboardError } from \"../types/dashboard.types\";\n\n// Query key factory for dashboard queries\nexport const dashboardQueryKeys = {\n all: [\"dashboard\"] as const,\n summary: () => [...dashboardQueryKeys.all, \"summary\"] as const,\n stats: () => [...dashboardQueryKeys.all, \"stats\"] as const,\n activity: (filters?: string[]) => [...dashboardQueryKeys.all, \"activity\", filters] as const,\n nextInvoice: () => [...dashboardQueryKeys.all, \"next-invoice\"] as const,\n};\n\n/**\n * Hook for fetching dashboard summary data\n */\nexport function useDashboardSummary() {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: dashboardQueryKeys.summary(),\n queryFn: async () => {\n if (!token) {\n throw {\n code: \"AUTHENTICATION_REQUIRED\",\n message: \"Authentication required to fetch dashboard data\",\n } as DashboardError;\n }\n\n try {\n return await dashboardService.getSummary();\n } catch (error) {\n // Transform API errors to DashboardError format\n if (error instanceof Error) {\n throw {\n code: \"FETCH_ERROR\",\n message: error.message,\n details: { originalError: error },\n } as DashboardError;\n }\n\n throw {\n code: \"UNKNOWN_ERROR\",\n message: \"An unexpected error occurred while fetching dashboard data\",\n details: { originalError: error },\n } as DashboardError;\n }\n },\n staleTime: 2 * 60 * 1000, // 2 minutes\n gcTime: 5 * 60 * 1000, // 5 minutes (formerly cacheTime)\n enabled: isAuthenticated && !!token,\n retry: (failureCount, error) => {\n // Don't retry authentication errors\n if (error?.code === \"AUTHENTICATION_REQUIRED\") {\n return false;\n }\n // Retry up to 3 times for other errors\n return failureCount < 3;\n },\n retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff\n });\n}\n\n/**\n * Hook for refreshing dashboard data\n */\nexport function useRefreshDashboard() {\n const queryClient = useQueryClient();\n const { isAuthenticated, token } = useAuthStore();\n\n const refreshDashboard = async () => {\n if (!isAuthenticated || !token) {\n throw new Error(\"Authentication required\");\n }\n\n // Invalidate and refetch dashboard queries\n await queryClient.invalidateQueries({\n queryKey: dashboardQueryKeys.all,\n });\n\n // Optionally fetch fresh data immediately\n return queryClient.fetchQuery({\n queryKey: dashboardQueryKeys.summary(),\n queryFn: () => dashboardService.refreshSummary(),\n });\n };\n\n return { refreshDashboard };\n}\n\n/**\n * Hook for fetching dashboard stats only (lightweight)\n */\nexport function useDashboardStats() {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: dashboardQueryKeys.stats(),\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n return dashboardService.getStats();\n },\n staleTime: 1 * 60 * 1000, // 1 minute\n gcTime: 3 * 60 * 1000, // 3 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n\n/**\n * Hook for fetching recent activity with filtering\n */\nexport function useDashboardActivity(filters?: string[], limit = 10) {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: dashboardQueryKeys.activity(filters),\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n return dashboardService.getRecentActivity(limit, filters);\n },\n staleTime: 30 * 1000, // 30 seconds\n gcTime: 2 * 60 * 1000, // 2 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n\n/**\n * Hook for fetching next invoice information\n */\nexport function useNextInvoice() {\n const { isAuthenticated, token } = useAuthStore();\n\n return useQuery({\n queryKey: dashboardQueryKeys.nextInvoice(),\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n return dashboardService.getNextInvoice();\n },\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/services/dashboard.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/services/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/stores/dashboard.store.ts","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'get' is defined but never used.","line":51,"column":11,"nodeType":null,"messageId":"unusedVar","endLine":51,"endColumn":14}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Dashboard Store\n * Local state management for dashboard UI state and preferences\n */\n\nimport { create } from \"zustand\";\nimport { persist } from \"zustand/middleware\";\nimport type { ActivityFilter } from \"../types/dashboard.types\";\n\ninterface DashboardUIState {\n // Activity filter state\n activityFilter: ActivityFilter;\n setActivityFilter: (filter: ActivityFilter) => void;\n\n // Dashboard preferences\n preferences: {\n showWelcomeMessage: boolean;\n compactView: boolean;\n autoRefresh: boolean;\n refreshInterval: number; // in seconds\n };\n updatePreferences: (preferences: Partial) => void;\n\n // UI state\n isRefreshing: boolean;\n setRefreshing: (refreshing: boolean) => void;\n\n // Error handling\n dismissedErrors: string[];\n dismissError: (errorId: string) => void;\n clearDismissedErrors: () => void;\n\n // Reset all state\n reset: () => void;\n}\n\nconst initialState = {\n activityFilter: \"all\" as ActivityFilter,\n preferences: {\n showWelcomeMessage: true,\n compactView: false,\n autoRefresh: false,\n refreshInterval: 300, // 5 minutes\n },\n isRefreshing: false,\n dismissedErrors: [],\n};\n\nexport const useDashboardStore = create()(\n persist(\n (set, get) => ({\n ...initialState,\n\n setActivityFilter: filter => {\n set({ activityFilter: filter });\n },\n\n updatePreferences: newPreferences => {\n set(state => ({\n preferences: {\n ...state.preferences,\n ...newPreferences,\n },\n }));\n },\n\n setRefreshing: refreshing => {\n set({ isRefreshing: refreshing });\n },\n\n dismissError: errorId => {\n set(state => ({\n dismissedErrors: [...state.dismissedErrors, errorId],\n }));\n },\n\n clearDismissedErrors: () => {\n set({ dismissedErrors: [] });\n },\n\n reset: () => {\n set(initialState);\n },\n }),\n {\n name: \"dashboard-store\",\n // Only persist preferences and dismissed errors\n partialize: state => ({\n preferences: state.preferences,\n dismissedErrors: state.dismissedErrors,\n }),\n }\n )\n);\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/stores/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/types/dashboard.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/utils/dashboard.utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/dashboard/utils/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/orders/containers/OrderDetail.tsx","messages":[{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":191,"column":22,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it's approved and ready for activation.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it‘s approved and ready for activation.\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it's approved and ready for activation.\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it’s approved and ready for activation.\n "},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":199,"column":26,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6167,6217],"text":"You'll receive an email confirmation once approved"},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6167,6217],"text":"You‘ll receive an email confirmation once approved"},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6167,6217],"text":"You'll receive an email confirmation once approved"},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6167,6217],"text":"You’ll receive an email confirmation once approved"},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useParams, useSearchParams } from \"next/navigation\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport {\n ClipboardDocumentCheckIcon,\n CheckCircleIcon,\n WifiIcon,\n DevicePhoneMobileIcon,\n LockClosedIcon,\n CubeIcon,\n} from \"@heroicons/react/24/outline\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { StatusPill } from \"@/components/ui/status-pill\";\nimport { ordersService } from \"@/features/orders/services/orders.service\";\n\ninterface OrderItem {\n id: string;\n quantity: number;\n unitPrice: number;\n totalPrice: number;\n product: {\n id: string;\n name: string;\n sku: string;\n whmcsProductId?: string;\n itemClass: string;\n billingCycle: string;\n };\n}\n\ninterface StatusInfo {\n label: string;\n color: string;\n bgColor: string;\n description: string;\n nextAction?: string;\n timeline?: string;\n}\n\ninterface OrderSummary {\n id: string;\n orderNumber?: string;\n status: string;\n orderType?: string;\n effectiveDate?: string;\n totalAmount?: number;\n accountName?: string;\n createdDate: string;\n lastModifiedDate: string;\n activationType?: string;\n activationStatus?: string;\n scheduledAt?: string;\n whmcsOrderId?: string;\n items?: OrderItem[];\n}\n\nconst getDetailedStatusInfo = (\n status: string,\n activationStatus?: string,\n activationType?: string,\n scheduledAt?: string\n): StatusInfo => {\n if (activationStatus === \"Activated\") {\n return {\n label: \"Service Active\",\n color: \"text-green-800\",\n bgColor: \"bg-green-50 border-green-200\",\n description: \"Your service is active and ready to use\",\n timeline: \"Service activated successfully\",\n };\n }\n if (status === \"Draft\" || status === \"Pending Review\") {\n return {\n label: \"Under Review\",\n color: \"text-blue-800\",\n bgColor: \"bg-blue-50 border-blue-200\",\n description: \"Our team is reviewing your order details\",\n nextAction: \"We will contact you within 1 business day with next steps\",\n timeline: \"Review typically takes 1 business day\",\n };\n }\n if (activationStatus === \"Scheduled\") {\n const scheduledDate = scheduledAt\n ? new Date(scheduledAt).toLocaleDateString(\"en-US\", {\n weekday: \"long\",\n month: \"long\",\n day: \"numeric\",\n })\n : \"soon\";\n return {\n label: \"Installation Scheduled\",\n color: \"text-orange-800\",\n bgColor: \"bg-orange-50 border-orange-200\",\n description: \"Your installation has been scheduled\",\n nextAction: `Installation scheduled for ${scheduledDate}`,\n timeline: \"Please be available during the scheduled time\",\n };\n }\n if (activationStatus === \"Activating\") {\n return {\n label: \"Setting Up Service\",\n color: \"text-purple-800\",\n bgColor: \"bg-purple-50 border-purple-200\",\n description: \"We're configuring your service\",\n nextAction: \"Installation team will contact you to schedule\",\n timeline: \"Setup typically takes 3-5 business days\",\n };\n }\n return {\n label: status || \"Processing\",\n color: \"text-gray-800\",\n bgColor: \"bg-gray-50 border-gray-200\",\n description: \"Your order is being processed\",\n timeline: \"We will update you as progress is made\",\n };\n};\n\nconst getOrderTypeIcon = (orderType?: string) => {\n switch (orderType) {\n case \"Internet\":\n return ;\n case \"SIM\":\n return ;\n case \"VPN\":\n return ;\n default:\n return ;\n }\n};\n\nconst calculateDetailedTotals = (items: OrderItem[]) => {\n let monthlyTotal = 0;\n let oneTimeTotal = 0;\n items.forEach(item => {\n if (item.product.billingCycle === \"Monthly\") monthlyTotal += item.totalPrice || 0;\n else oneTimeTotal += item.totalPrice || 0;\n });\n return { monthlyTotal, oneTimeTotal };\n};\n\nexport function OrderDetailContainer() {\n const params = useParams<{ id: string }>();\n const searchParams = useSearchParams();\n const [data, setData] = useState(null);\n const [error, setError] = useState(null);\n const isNewOrder = searchParams.get(\"status\") === \"success\";\n\n useEffect(() => {\n let mounted = true;\n const fetchStatus = async () => {\n try {\n const order = await ordersService.getOrderById(params.id);\n if (mounted) setData(order || null);\n } catch (e) {\n if (mounted) setError(e instanceof Error ? e.message : \"Failed to load order\");\n }\n };\n void fetchStatus();\n const interval = setInterval(() => {\n void fetchStatus();\n }, 5000);\n return () => {\n mounted = false;\n clearInterval(interval);\n };\n }, [params.id]);\n\n return (\n }\n title={data ? `${data.orderType} Service Order` : \"Order Details\"}\n description={\n data\n ? `Order #${data.orderNumber || String(data.id).slice(-8)}`\n : \"Loading order details...\"\n }\n >\n {error &&
{error}
}\n {isNewOrder && (\n
\n
\n \n
\n

\n Order Submitted Successfully!\n

\n

\n Your order has been created and submitted for processing. We will notify you as soon\n as it's approved and ready for activation.\n

\n
\n

\n What happens next:\n

\n
    \n
  • Our team will review your order (within 1 business day)
  • \n
  • You'll receive an email confirmation once approved
  • \n
  • We will schedule activation based on your preferences
  • \n
  • This page will update automatically as your order progresses
  • \n
\n
\n
\n
\n
\n )}\n {data &&\n (() => {\n const statusInfo = getDetailedStatusInfo(\n data.status,\n data.activationStatus,\n data.activationType,\n data.scheduledAt\n );\n const statusVariant = statusInfo.label.includes(\"Active\")\n ? \"success\"\n : statusInfo.label.includes(\"Review\") ||\n statusInfo.label.includes(\"Setting Up\") ||\n statusInfo.label.includes(\"Scheduled\")\n ? \"info\"\n : \"neutral\";\n return (\n Status}\n >\n
\n
{statusInfo.description}
\n \n
\n {statusInfo.nextAction && (\n
\n
\n \n \n \n Next Steps\n
\n

{statusInfo.nextAction}

\n
\n )}\n {statusInfo.timeline && (\n
{statusInfo.timeline}
\n )}\n \n );\n })()}\n {data && (\n \n {getOrderTypeIcon(data.orderType)}\n

Order Items

\n \n }\n >\n {!data.items || data.items.length === 0 ? (\n
No items on this order.
\n ) : (\n
\n {data.items.map(item => (\n \n
\n
{item.product.name}
\n
SKU: {item.product.sku}
\n
{item.product.billingCycle}
\n
\n
\n
Qty: {item.quantity}
\n
\n ¥{(item.totalPrice || 0).toLocaleString()}\n
\n
\n
\n ))}\n {(() => {\n const totals = calculateDetailedTotals(data.items || []);\n return (\n
\n
\n
\n ¥{totals.monthlyTotal.toLocaleString()}{\" \"}\n /mo\n
\n {totals.oneTimeTotal > 0 && (\n
\n ¥{totals.oneTimeTotal.toLocaleString()}{\" \"}\n one-time\n
\n )}\n
\n
\n );\n })()}\n \n )}\n \n )}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/orders/containers/OrdersList.tsx","messages":[{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":192,"column":54,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6072,6106],"text":"You haven't placed any orders yet."},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6072,6106],"text":"You haven‘t placed any orders yet."},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6072,6106],"text":"You haven't placed any orders yet."},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6072,6106],"text":"You haven’t placed any orders yet."},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useEffect, useState, Suspense } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { PageLayout } from \"@/components/layout/PageLayout\";\nimport {\n ClipboardDocumentListIcon,\n CheckCircleIcon,\n WifiIcon,\n DevicePhoneMobileIcon,\n LockClosedIcon,\n CubeIcon,\n} from \"@heroicons/react/24/outline\";\nimport { StatusPill } from \"@/components/ui/status-pill\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { AnimatedCard } from \"@/components/ui\";\nimport { ordersService } from \"@/features/orders/services/orders.service\";\n\ninterface OrderSummary {\n id: string | number;\n orderNumber?: string;\n status: string;\n orderType?: string;\n effectiveDate?: string;\n totalAmount?: number;\n createdDate: string;\n activationStatus?: string;\n itemSummary?: string;\n itemsSummary?: Array<{\n name?: string;\n sku?: string;\n itemClass?: string;\n quantity: number;\n unitPrice?: number;\n totalPrice?: number;\n billingCycle?: string;\n }>;\n}\n\ninterface StatusInfo {\n label: string;\n color: string;\n bgColor: string;\n description: string;\n nextAction?: string;\n}\n\nfunction OrdersSuccessBanner() {\n const searchParams = useSearchParams();\n const showSuccess = searchParams.get(\"status\") === \"success\";\n if (!showSuccess) return null;\n return (\n
\n
\n \n
\n

\n Order Submitted Successfully!\n

\n

\n Your order has been created and is now being processed. You can track its progress\n below.\n

\n
\n
\n
\n );\n}\n\nexport function OrdersListContainer() {\n const router = useRouter();\n const [orders, setOrders] = useState([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n const fetchOrders = async () => {\n try {\n const list = await ordersService.getMyOrders();\n setOrders(list);\n } catch (e) {\n setError(e instanceof Error ? e.message : \"Failed to load orders\");\n } finally {\n setLoading(false);\n }\n };\n void fetchOrders();\n }, []);\n\n const getStatusInfo = (status: string, activationStatus?: string): StatusInfo => {\n if (activationStatus === \"Activated\") {\n return {\n label: \"Active\",\n color: \"text-green-800\",\n bgColor: \"bg-green-100\",\n description: \"Your service is active and ready to use\",\n };\n }\n if (status === \"Draft\" || status === \"Pending Review\") {\n return {\n label: \"Under Review\",\n color: \"text-blue-800\",\n bgColor: \"bg-blue-100\",\n description: \"We're reviewing your order\",\n nextAction: \"We'll contact you within 1 business day\",\n };\n }\n if (activationStatus === \"Activating\") {\n return {\n label: \"Setting Up\",\n color: \"text-orange-800\",\n bgColor: \"bg-orange-100\",\n description: \"We're preparing your service\",\n nextAction: \"Installation will be scheduled soon\",\n };\n }\n return {\n label: status || \"Processing\",\n color: \"text-gray-800\",\n bgColor: \"bg-gray-100\",\n description: \"Order is being processed\",\n };\n };\n\n const getServiceTypeDisplay = (orderType?: string) => {\n switch (orderType) {\n case \"Internet\":\n return { icon: , label: \"Internet Service\" };\n case \"SIM\":\n return { icon: , label: \"Mobile Service\" };\n case \"VPN\":\n return { icon: , label: \"VPN Service\" };\n default:\n return { icon: , label: \"Service\" };\n }\n };\n\n const getServiceSummary = (order: OrderSummary) => {\n if (order.itemsSummary && order.itemsSummary.length > 0) {\n const mainItem = order.itemsSummary[0];\n const additionalCount = order.itemsSummary.length - 1;\n let summary = mainItem.name || \"Service\";\n if (additionalCount > 0) summary += ` +${additionalCount} more`;\n return summary;\n }\n return order.itemSummary || \"Service package\";\n };\n\n const calculateOrderTotals = (order: OrderSummary) => {\n let monthlyTotal = 0;\n let oneTimeTotal = 0;\n if (order.itemsSummary && order.itemsSummary.length > 0) {\n order.itemsSummary.forEach(item => {\n const totalPrice = item.totalPrice || 0;\n const billingCycle = item.billingCycle?.toLowerCase() || \"\";\n if (billingCycle === \"monthly\") monthlyTotal += totalPrice;\n else oneTimeTotal += totalPrice;\n });\n } else {\n monthlyTotal = order.totalAmount || 0;\n }\n return { monthlyTotal, oneTimeTotal } as const;\n };\n\n return (\n }\n title=\"My Orders\"\n description=\"View and track all your orders\"\n >\n \n \n \n\n {error && (\n
\n

{error}

\n
\n )}\n\n {loading ? (\n
\n
\n \n

Loading your orders...

\n
\n
\n ) : orders.length === 0 ? (\n \n \n

No orders yet

\n

You haven't placed any orders yet.

\n router.push(\"/catalog\")}\n className=\"bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors\"\n >\n Browse Catalog\n \n
\n ) : (\n
\n {orders.map(order => {\n const statusInfo = getStatusInfo(order.status, order.activationStatus);\n const serviceType = getServiceTypeDisplay(order.orderType);\n const serviceSummary = getServiceSummary(order);\n return (\n router.push(`/orders/${order.id}`)}\n >\n
\n
\n
{serviceType.icon}
\n
\n

\n {serviceType.label}\n

\n

\n Order #{order.orderNumber || String(order.id).slice(-8)} •{\" \"}\n {new Date(order.createdDate).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n })}\n

\n
\n
\n
\n \n
\n
\n
\n
\n
\n

{serviceSummary}

\n

{statusInfo.description}

\n {statusInfo.nextAction && (\n

\n {statusInfo.nextAction}\n

\n )}\n
\n {(() => {\n const totals = calculateOrderTotals(order);\n if (totals.monthlyTotal <= 0 && totals.oneTimeTotal <= 0) return null;\n return (\n
\n
\n

\n ¥{totals.monthlyTotal.toLocaleString()}\n

\n

per month

\n {totals.oneTimeTotal > 0 && (\n <>\n

\n ¥{totals.oneTimeTotal.toLocaleString()}\n

\n

one-time

\n \n )}\n
\n
\n

* Additional fees may apply

\n

(e.g., weekend installation)

\n
\n
\n );\n })()}\n
\n
\n
\n Click to view details\n \n \n \n
\n \n );\n })}\n
\n )}\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/orders/services/orders.service.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":9,"column":25,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":9,"endColumn":28,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[176,179],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[176,179],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":14,"column":26,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":14,"endColumn":29,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[323,326],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[323,326],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":20,"column":65,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":20,"endColumn":68,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[587,590],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[587,590],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Orders Service\n * Centralized methods for orders API operations\n */\n\nimport { apiClient } from \"@/lib/api/client\";\n\nexport class OrdersService {\n async getMyOrders(): Promise {\n const res = await apiClient.get(\"/orders/user\");\n return (res.data as T[]) || [];\n }\n\n async getOrderById(id: string): Promise {\n const res = await apiClient.get(`/orders/${id}`);\n return res.data as T;\n }\n\n async createOrder(orderData: unknown): Promise {\n const res = await apiClient.post(\"/orders\", orderData as any);\n return res.data as T;\n }\n}\n\nexport const ordersService = new OrdersService();\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/service-management/components/ServiceManagementSection.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/service-management/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/ChangePlanModal.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/DataUsageChart.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/SimActions.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'activeInfo' is assigned a value but never used.","line":57,"column":12,"nodeType":null,"messageId":"unusedVar","endLine":57,"endColumn":22}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport React, { useState, forwardRef } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport {\n PlusIcon,\n ArrowPathIcon,\n XMarkIcon,\n ExclamationTriangleIcon,\n CheckCircleIcon,\n Cog6ToothIcon,\n} from \"@heroicons/react/24/outline\";\nimport { Button } from \"@/components/ui/button\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { TopUpModal } from \"./TopUpModal\";\nimport { ChangePlanModal } from \"./ChangePlanModal\";\nimport { simActionsService } from \"@/features/subscriptions/services/sim-actions.service\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SimActionsProps {\n subscriptionId: number;\n simType: \"physical\" | \"esim\";\n status: string;\n onTopUpSuccess?: () => void;\n onPlanChangeSuccess?: () => void;\n onCancelSuccess?: () => void;\n onReissueSuccess?: () => void;\n embedded?: boolean; // when true, render content without card container\n currentPlanCode?: string;\n className?: string;\n}\n\nexport const SimActions = forwardRef(\n (\n {\n subscriptionId,\n simType,\n status,\n onTopUpSuccess,\n onPlanChangeSuccess,\n onCancelSuccess,\n onReissueSuccess,\n embedded = false,\n currentPlanCode,\n className,\n },\n ref\n ) => {\n const router = useRouter();\n const [showTopUpModal, setShowTopUpModal] = useState(false);\n const [showCancelConfirm, setShowCancelConfirm] = useState(false);\n const [showReissueConfirm, setShowReissueConfirm] = useState(false);\n const [loading, setLoading] = useState(null);\n const [error, setError] = useState(null);\n const [success, setSuccess] = useState(null);\n const [showChangePlanModal, setShowChangePlanModal] = useState(false);\n const [activeInfo, setActiveInfo] = useState<\n \"topup\" | \"reissue\" | \"cancel\" | \"changePlan\" | null\n >(null);\n\n const isActive = status === \"active\";\n const canTopUp = isActive;\n const canReissue = isActive && simType === \"esim\";\n const canCancel = isActive;\n\n const handleReissueEsim = async () => {\n setLoading(\"reissue\");\n setError(null);\n\n try {\n await simActionsService.reissueEsim(subscriptionId);\n\n setSuccess(\"eSIM profile reissued successfully\");\n setShowReissueConfirm(false);\n onReissueSuccess?.();\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : \"Failed to reissue eSIM profile\");\n } finally {\n setLoading(null);\n }\n };\n\n const handleCancelSim = async () => {\n setLoading(\"cancel\");\n setError(null);\n\n try {\n await simActionsService.cancel(subscriptionId, {});\n\n setSuccess(\"SIM service cancelled successfully\");\n setShowCancelConfirm(false);\n onCancelSuccess?.();\n } catch (error: unknown) {\n setError(error instanceof Error ? error.message : \"Failed to cancel SIM service\");\n } finally {\n setLoading(null);\n }\n };\n\n // Clear success/error messages after 5 seconds\n React.useEffect(() => {\n if (success || error) {\n const timer = setTimeout(() => {\n setSuccess(null);\n setError(null);\n }, 5000);\n return () => clearTimeout(timer);\n }\n return;\n }, [success, error]);\n\n const content = (\n <>\n {/* Header */}\n {!embedded && (\n
\n
\n \n
\n
\n

SIM Management Actions

\n

Manage your SIM service

\n
\n
\n )}\n {/* Status Messages */}\n {success && (\n
\n
\n \n

{success}

\n
\n
\n )}\n\n {error && (\n
\n
\n \n

{error}

\n
\n
\n )}\n\n {!isActive && (\n
\n
\n \n

\n SIM management actions are only available for active services.\n

\n
\n
\n )}\n\n {/* Action Buttons */}\n
\n {/* Top Up Data - Primary Action */}\n
\n
\n
\n \n
\n
\n
\n

Top Up Data

\n

\n Add additional data quota to your SIM service\n

\n
\n
\n {\n setActiveInfo(\"topup\");\n try {\n router.push(`/subscriptions/${subscriptionId}/sim/top-up`);\n } catch {\n setShowTopUpModal(true);\n }\n }}\n >\n Top Up\n \n
\n
\n\n {/* Reissue eSIM (only for eSIMs) */}\n {simType === \"esim\" && (\n
\n
\n
\n \n
\n
\n
\n

Reissue eSIM

\n

\n Generate a new eSIM profile for download\n

\n
\n
\n {\n setActiveInfo(\"reissue\");\n try {\n router.push(`/subscriptions/${subscriptionId}/sim/reissue`);\n } catch {\n setShowReissueConfirm(true);\n }\n }}\n >\n Reissue\n \n
\n
\n )}\n\n {/* Change Plan - Secondary Action */}\n
\n
\n
\n \n \n \n
\n
\n
\n

Change Plan

\n

Switch to a different data plan

\n
\n
\n {\n setActiveInfo(\"changePlan\");\n try {\n router.push(`/subscriptions/${subscriptionId}/sim/change-plan`);\n } catch {\n setShowChangePlanModal(true);\n }\n }}\n >\n Change Plan\n \n
\n
\n\n {/* Cancel SIM - Destructive Action */}\n
\n
\n
\n \n
\n
\n
\n

Cancel SIM

\n

Permanently cancel your SIM service

\n
\n
\n {\n setActiveInfo(\"cancel\");\n try {\n router.push(`/subscriptions/${subscriptionId}/sim/cancel`);\n } catch {\n setShowCancelConfirm(true);\n }\n }}\n >\n Cancel SIM\n \n
\n
\n
\n \n );\n\n if (embedded) {\n return (\n
\n {content}\n {/* Modals and confirmations */}\n {renderModals()}\n
\n );\n }\n\n return (\n }\n className={cn(\"\", className)}\n >\n {content}\n {/* Modals and confirmations */}\n {renderModals()}\n \n );\n\n function renderModals() {\n return (\n <>\n {/* Top Up Modal */}\n {showTopUpModal && (\n {\n setShowTopUpModal(false);\n setActiveInfo(null);\n }}\n onSuccess={() => {\n setShowTopUpModal(false);\n setSuccess(\"Data top-up completed successfully\");\n onTopUpSuccess?.();\n }}\n onError={message => setError(message)}\n />\n )}\n\n {/* Change Plan Modal */}\n {showChangePlanModal && (\n {\n setShowChangePlanModal(false);\n setActiveInfo(null);\n }}\n onSuccess={() => {\n setShowChangePlanModal(false);\n setSuccess(\"SIM plan change submitted successfully\");\n onPlanChangeSuccess?.();\n }}\n onError={message => setError(message)}\n />\n )}\n\n {/* Reissue eSIM Confirmation */}\n {showReissueConfirm && (\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n

\n Reissue eSIM Profile\n

\n
\n

\n This will generate a new eSIM profile for download. Your current eSIM\n will remain active until you activate the new profile.\n

\n
\n
\n
\n
\n
\n void handleReissueEsim()}\n disabled={loading === \"reissue\"}\n className=\"w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50\"\n >\n {loading === \"reissue\" ? \"Processing...\" : \"Reissue eSIM\"}\n \n {\n setShowReissueConfirm(false);\n setActiveInfo(null);\n }}\n disabled={loading === \"reissue\"}\n className=\"mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm\"\n >\n Back\n \n
\n
\n
\n
\n )}\n\n {/* Cancel Confirmation */}\n {showCancelConfirm && (\n
\n
\n
\n
\n
\n
\n
\n \n
\n
\n

\n Cancel SIM Service\n

\n
\n

\n Are you sure you want to cancel this SIM service? This action cannot be\n undone and will permanently terminate your service.\n

\n
\n
\n
\n
\n
\n void handleCancelSim()}\n disabled={loading === \"cancel\"}\n className=\"w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm disabled:opacity-50\"\n >\n {loading === \"cancel\" ? \"Processing...\" : \"Cancel SIM\"}\n \n {\n setShowCancelConfirm(false);\n setActiveInfo(null);\n }}\n disabled={loading === \"cancel\"}\n className=\"mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm\"\n >\n Back\n \n
\n
\n
\n
\n )}\n \n );\n }\n }\n);\n\nSimActions.displayName = \"SimActions\";\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/SimDetailsCard.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/SimFeatureToggles.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/SimManagementSection.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ExclamationTriangleIcon' is defined but never used.","line":6,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":6,"endColumn":26},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ArrowPathIcon' is defined but never used.","line":7,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":7,"endColumn":16}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport React, { useState, useEffect, useCallback } from \"react\";\nimport {\n DevicePhoneMobileIcon,\n ExclamationTriangleIcon,\n ArrowPathIcon,\n} from \"@heroicons/react/24/outline\";\nimport { SimDetailsCard, type SimDetails } from \"./SimDetailsCard\";\nimport { DataUsageChart, type SimUsage } from \"./DataUsageChart\";\nimport { SimActions } from \"./SimActions\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { LoadingSpinner } from \"@/components/ui/loading-spinner\";\nimport { ErrorState } from \"@/components/ui/error-state\";\nimport { simActionsService } from \"@/features/subscriptions/services/sim-actions.service\";\nimport { SimFeatureToggles } from \"./SimFeatureToggles\";\n\ninterface SimManagementSectionProps {\n subscriptionId: number;\n}\n\ninterface SimInfo {\n details: SimDetails;\n usage: SimUsage;\n}\n\nexport function SimManagementSection({ subscriptionId }: SimManagementSectionProps) {\n const [simInfo, setSimInfo] = useState(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(null);\n\n const fetchSimInfo = useCallback(async () => {\n try {\n setError(null);\n\n const info = await simActionsService.getSimInfo(subscriptionId);\n setSimInfo((info as SimInfo) || null);\n } catch (err: unknown) {\n const hasStatus = (v: unknown): v is { status: number } =>\n typeof v === \"object\" &&\n v !== null &&\n \"status\" in v &&\n typeof (v as { status: unknown }).status === \"number\";\n if (hasStatus(err) && err.status === 400) {\n // Not a SIM subscription - this component shouldn't be shown\n setError(\"This subscription is not a SIM service\");\n } else {\n setError(err instanceof Error ? err.message : \"Failed to load SIM information\");\n }\n } finally {\n setLoading(false);\n }\n }, [subscriptionId]);\n\n useEffect(() => {\n void fetchSimInfo();\n }, [fetchSimInfo]);\n\n const handleRefresh = () => {\n setLoading(true);\n void fetchSimInfo();\n };\n\n const handleActionSuccess = () => {\n // Refresh SIM info after any successful action\n void fetchSimInfo();\n };\n\n if (loading) {\n return (\n
\n }>\n
\n
\n \n

Loading your SIM service details...

\n
\n
\n
\n
\n );\n }\n\n if (error) {\n return (\n }>\n \n \n );\n }\n\n if (!simInfo) {\n return null;\n }\n\n return (\n
\n {/* SIM Details and Usage - Main Content */}\n
\n {/* Main Content Area - Actions and Settings (Left Side) */}\n
\n \n \n
\n

Modify service options

\n \n
\n
\n
\n\n {/* Sidebar - Compact Info (Right Side) */}\n
\n {/* Details + Usage combined card for mobile-first */}\n \n
\n \n \n
\n
\n\n {/* Important Information Card */}\n
\n
\n
\n \n \n \n
\n

Important Information

\n
\n
    \n
  • \n \n Data usage is updated in real-time and may take a few minutes to reflect recent\n activity\n
  • \n
  • \n \n Top-up data will be available immediately after successful processing\n
  • \n
  • \n \n SIM cancellation is permanent and cannot be undone\n
  • \n {simInfo.details.simType === \"esim\" && (\n
  • \n \n eSIM profile reissue will provide a new QR code for activation\n
  • \n )}\n
\n
\n\n {/* (On desktop, details+usage are above; on mobile they appear first since this section is above actions) */}\n
\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/components/TopUpModal.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/sim-management/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/SubscriptionActions.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'isInternetService' is assigned a value but never used.","line":74,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":74,"endColumn":26},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'isVpnService' is assigned a value but never used.","line":78,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":78,"endColumn":21},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":193,"column":25,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":193,"endColumn":40},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":204,"column":25,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":204,"endColumn":39},{"ruleId":"@typescript-eslint/no-misused-promises","severity":2,"message":"Promise-returning function provided to attribute where a void return was expected.","line":269,"column":29,"nodeType":"JSXExpressionContainer","messageId":"voidReturnAttribute","endLine":269,"endColumn":43}],"suppressedMessages":[],"errorCount":3,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport {\n PauseIcon,\n PlayIcon,\n XMarkIcon,\n ArrowUpIcon,\n ArrowDownIcon,\n DocumentTextIcon,\n CreditCardIcon,\n Cog6ToothIcon,\n ExclamationTriangleIcon,\n} from \"@heroicons/react/24/outline\";\nimport { Button } from \"@/components/ui/button\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport type { Subscription } from \"@customer-portal/shared\";\nimport { cn } from \"@/lib/utils\";\nimport { useSubscriptionAction } from \"../hooks\";\n\ninterface SubscriptionActionsProps {\n subscription: Subscription;\n onActionSuccess?: () => void;\n className?: string;\n}\n\ninterface ActionButtonProps {\n icon: React.ReactNode;\n label: string;\n description: string;\n variant?: \"default\" | \"destructive\" | \"outline\" | \"secondary\";\n disabled?: boolean;\n onClick: () => void;\n}\n\nconst ActionButton = ({\n icon,\n label,\n description,\n variant = \"outline\",\n disabled,\n onClick,\n}: ActionButtonProps) => (\n
\n
{icon}
\n
\n

{label}

\n

{description}

\n
\n
\n \n
\n
\n);\n\nexport function SubscriptionActions({\n subscription,\n onActionSuccess,\n className,\n}: SubscriptionActionsProps) {\n const router = useRouter();\n const [loading, setLoading] = useState(null);\n const subscriptionAction = useSubscriptionAction();\n\n const isActive = subscription.status === \"Active\";\n const isSuspended = subscription.status === \"Suspended\";\n const isCancelled = subscription.status === \"Cancelled\" || subscription.status === \"Terminated\";\n const isPending = subscription.status === \"Pending\";\n\n const isSimService = subscription.productName.toLowerCase().includes(\"sim\");\n const isInternetService =\n subscription.productName.toLowerCase().includes(\"internet\") ||\n subscription.productName.toLowerCase().includes(\"broadband\") ||\n subscription.productName.toLowerCase().includes(\"fiber\");\n const isVpnService = subscription.productName.toLowerCase().includes(\"vpn\");\n\n const handleSuspend = async () => {\n setLoading(\"suspend\");\n try {\n await subscriptionAction.mutateAsync({\n id: subscription.id,\n action: \"suspend\",\n });\n onActionSuccess?.();\n } catch (error) {\n console.error(\"Failed to suspend subscription:\", error);\n } finally {\n setLoading(null);\n }\n };\n\n const handleResume = async () => {\n setLoading(\"resume\");\n try {\n await subscriptionAction.mutateAsync({\n id: subscription.id,\n action: \"resume\",\n });\n onActionSuccess?.();\n } catch (error) {\n console.error(\"Failed to resume subscription:\", error);\n } finally {\n setLoading(null);\n }\n };\n\n const handleCancel = async () => {\n if (\n !confirm(\"Are you sure you want to cancel this subscription? This action cannot be undone.\")\n ) {\n return;\n }\n\n setLoading(\"cancel\");\n try {\n await subscriptionAction.mutateAsync({\n id: subscription.id,\n action: \"cancel\",\n });\n onActionSuccess?.();\n } catch (error) {\n console.error(\"Failed to cancel subscription:\", error);\n } finally {\n setLoading(null);\n }\n };\n\n const handleUpgrade = () => {\n router.push(`/catalog?upgrade=${subscription.id}`);\n };\n\n const handleDowngrade = () => {\n router.push(`/catalog?downgrade=${subscription.id}`);\n };\n\n const handleViewInvoices = () => {\n router.push(`/subscriptions/${subscription.id}#billing`);\n };\n\n const handleManagePayment = () => {\n router.push(\"/billing/payments\");\n };\n\n const handleSimManagement = () => {\n router.push(`/subscriptions/${subscription.id}#sim-management`);\n };\n\n const handleServiceSettings = () => {\n router.push(`/subscriptions/${subscription.id}/settings`);\n };\n\n return (\n }\n className={cn(\"\", className)}\n >\n
\n {/* Service Management Actions */}\n
\n

Service Management

\n
\n {/* SIM Management - Only for SIM services */}\n {isSimService && isActive && (\n }\n label=\"SIM Management\"\n description=\"Manage data usage, top-up, and SIM settings\"\n onClick={handleSimManagement}\n />\n )}\n\n {/* Service Settings - Available for all active services */}\n {isActive && (\n }\n label=\"Service Settings\"\n description=\"Configure service-specific settings and preferences\"\n onClick={handleServiceSettings}\n />\n )}\n\n {/* Suspend/Resume Actions */}\n {isActive && (\n }\n label=\"Suspend Service\"\n description=\"Temporarily suspend this service\"\n variant=\"outline\"\n onClick={handleSuspend}\n disabled={loading === \"suspend\"}\n />\n )}\n\n {isSuspended && (\n }\n label=\"Resume Service\"\n description=\"Resume suspended service\"\n variant=\"outline\"\n onClick={handleResume}\n disabled={loading === \"resume\"}\n />\n )}\n
\n
\n\n {/* Plan Management Actions */}\n {(isActive || isSuspended) && !subscription.cycle.includes(\"One-time\") && (\n
\n

Plan Management

\n
\n }\n label=\"Upgrade Plan\"\n description=\"Upgrade to a higher tier plan with more features\"\n onClick={handleUpgrade}\n />\n\n }\n label=\"Downgrade Plan\"\n description=\"Switch to a lower tier plan\"\n onClick={handleDowngrade}\n />\n
\n
\n )}\n\n {/* Billing Actions */}\n
\n

Billing & Payment

\n
\n }\n label=\"View Invoices\"\n description=\"View billing history and download invoices\"\n onClick={handleViewInvoices}\n />\n\n }\n label=\"Manage Payment\"\n description=\"Update payment methods and billing information\"\n onClick={handleManagePayment}\n />\n
\n
\n\n {/* Cancellation Actions */}\n {!isCancelled && !isPending && (\n
\n

Cancellation

\n
\n
\n \n
\n
Cancel Subscription
\n

\n Permanently cancel this subscription. This action cannot be undone and you will\n lose access to the service.\n

\n }\n >\n Cancel Subscription\n \n
\n
\n
\n
\n )}\n\n {/* Status Information */}\n {(isCancelled || isPending) && (\n
\n
\n \n
\n {isCancelled ? \"Subscription Cancelled\" : \"Subscription Pending\"}\n
\n
\n

\n {isCancelled\n ? \"This subscription has been cancelled and is no longer active. No further actions are available.\"\n : \"This subscription is pending activation. Actions will be available once the subscription is active.\"}\n

\n
\n )}\n
\n \n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/SubscriptionCard.tsx","messages":[{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'Link' is defined but never used.","line":4,"column":8,"nodeType":null,"messageId":"unusedVar","endLine":4,"endColumn":12},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ServerIcon' is defined but never used.","line":7,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":7,"endColumn":13}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport Link from \"next/link\";\nimport { format } from \"date-fns\";\nimport {\n ServerIcon,\n CheckCircleIcon,\n ExclamationTriangleIcon,\n ClockIcon,\n XCircleIcon,\n CalendarIcon,\n ArrowTopRightOnSquareIcon,\n} from \"@heroicons/react/24/outline\";\nimport { StatusPill } from \"@/components/ui/status-pill\";\nimport { Button } from \"@/components/ui/button\";\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { formatCurrency, getCurrencyLocale } from \"@/utils/currency\";\nimport type { Subscription } from \"@customer-portal/shared\";\nimport { cn } from \"@/lib/utils\";\n\ninterface SubscriptionCardProps {\n subscription: Subscription;\n variant?: \"list\" | \"grid\";\n showActions?: boolean;\n onViewClick?: (subscription: Subscription) => void;\n className?: string;\n}\n\nconst getStatusIcon = (status: string) => {\n switch (status) {\n case \"Active\":\n return ;\n case \"Suspended\":\n return ;\n case \"Pending\":\n return ;\n case \"Cancelled\":\n case \"Terminated\":\n return ;\n default:\n return ;\n }\n};\n\nconst getStatusVariant = (status: string) => {\n switch (status) {\n case \"Active\":\n return \"success\" as const;\n case \"Suspended\":\n return \"warning\" as const;\n case \"Pending\":\n return \"info\" as const;\n case \"Cancelled\":\n case \"Terminated\":\n return \"neutral\" as const;\n default:\n return \"neutral\" as const;\n }\n};\n\nconst formatDate = (dateString: string | undefined) => {\n if (!dateString) return \"N/A\";\n try {\n return format(new Date(dateString), \"MMM d, yyyy\");\n } catch {\n return \"Invalid date\";\n }\n};\n\nconst getBillingCycleLabel = (cycle: string) => {\n const name = cycle.toLowerCase();\n const looksLikeActivation = name.includes(\"activation\") || name.includes(\"setup\");\n return looksLikeActivation ? \"One-time\" : cycle;\n};\n\nexport const SubscriptionCard = forwardRef(\n ({ subscription, variant = \"list\", showActions = true, onViewClick, className }, ref) => {\n const handleViewClick = () => {\n if (onViewClick) {\n onViewClick(subscription);\n }\n };\n\n if (variant === \"grid\") {\n return (\n \n
\n {/* Header */}\n
\n
\n {getStatusIcon(subscription.status)}\n
\n

\n {subscription.productName}\n

\n

Service ID: {subscription.serviceId}

\n
\n
\n \n
\n\n {/* Details */}\n
\n
\n

Price

\n

\n {formatCurrency(subscription.amount, {\n currency: \"JPY\",\n locale: getCurrencyLocale(\"JPY\"),\n })}\n

\n

{getBillingCycleLabel(subscription.cycle)}

\n
\n
\n

Next Due

\n
\n \n

{formatDate(subscription.nextDue)}

\n
\n
\n
\n\n {/* Actions */}\n {showActions && (\n
\n

\n Created {formatDate(subscription.registrationDate)}\n

\n
\n }\n >\n View\n \n
\n
\n )}\n
\n
\n );\n }\n\n // List variant (default)\n return (\n \n
\n
\n {getStatusIcon(subscription.status)}\n
\n
\n

\n {subscription.productName}\n

\n \n
\n

Service ID: {subscription.serviceId}

\n
\n
\n\n
\n
\n

\n {formatCurrency(subscription.amount, {\n currency: \"JPY\",\n locale: getCurrencyLocale(\"JPY\"),\n })}\n

\n

{getBillingCycleLabel(subscription.cycle)}

\n
\n\n
\n
\n \n

{formatDate(subscription.nextDue)}

\n
\n

Next due

\n
\n\n {showActions && (\n
\n }\n >\n View\n \n
\n )}\n
\n
\n
\n );\n }\n);\n\nSubscriptionCard.displayName = \"SubscriptionCard\";\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/SubscriptionDetails.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/SubscriptionStatusBadge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/components/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/containers/SimCancel.tsx","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":51,"column":69,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":51,"endColumn":72,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1981,1984],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1981,1984],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async arrow function 'fetchEmail' has no 'await' expression.","line":61,"column":33,"nodeType":"ArrowFunctionExpression","messageId":"missingAwait","endLine":61,"endColumn":35,"suggestions":[{"messageId":"removeAsync","fix":{"range":[2264,2270],"text":""},"desc":"Remove 'async'."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\"use client\";\n\nimport Link from \"next/link\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport { useEffect, useMemo, useState, type ReactNode } from \"react\";\nimport { simActionsService } from \"@/features/subscriptions/services/sim-actions.service\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport type { SimDetails } from \"@/features/sim-management/components/SimDetailsCard\";\n\ntype Step = 1 | 2 | 3;\n\nfunction Notice({ title, children }: { title: string; children: ReactNode }) {\n return (\n
\n
{title}
\n
{children}
\n
\n );\n}\n\nfunction InfoRow({ label, value }: { label: string; value: string }) {\n return (\n
\n
{label}
\n
{value}
\n
\n );\n}\n\nexport function SimCancelContainer() {\n const params = useParams();\n const router = useRouter();\n const subscriptionId = parseInt(params.id as string);\n\n const [step, setStep] = useState(1);\n const [loading, setLoading] = useState(false);\n const [details, setDetails] = useState(null);\n const [error, setError] = useState(null);\n const [message, setMessage] = useState(null);\n const [acceptTerms, setAcceptTerms] = useState(false);\n const [confirmMonthEnd, setConfirmMonthEnd] = useState(false);\n const [cancelMonth, setCancelMonth] = useState(\"\");\n const [email, setEmail] = useState(\"\");\n const [email2, setEmail2] = useState(\"\");\n const [notes, setNotes] = useState(\"\");\n const [registeredEmail, setRegisteredEmail] = useState(null);\n\n useEffect(() => {\n const fetchDetails = async () => {\n try {\n const info = await simActionsService.getSimInfo(subscriptionId);\n setDetails(info?.details || null);\n } catch (e: unknown) {\n setError(e instanceof Error ? e.message : \"Failed to load SIM details\");\n }\n };\n void fetchDetails();\n }, [subscriptionId]);\n\n useEffect(() => {\n const fetchEmail = async () => {\n try {\n // Prefer auth store email; fallback to address fetch only if needed\n const emailFromStore = useAuthStore.getState().user?.email;\n if (emailFromStore) {\n setRegisteredEmail(emailFromStore);\n return;\n }\n // If needed, get via /me/address payload enrichment in future; skip extra call for now\n } catch {\n // ignore\n }\n };\n void fetchEmail();\n }, []);\n\n const monthOptions = useMemo(() => {\n const opts: { value: string; label: string }[] = [];\n const now = new Date();\n for (let i = 1; i <= 12; i++) {\n const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + i, 1));\n const y = d.getUTCFullYear();\n const m = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n opts.push({ value: `${y}${m}`, label: `${y} / ${m}` });\n }\n return opts;\n }, []);\n\n const canProceedStep2 = !!details;\n const emailPattern = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n const emailProvided = email.trim().length > 0 || email2.trim().length > 0;\n const emailValid =\n !emailProvided || (emailPattern.test(email.trim()) && emailPattern.test(email2.trim()));\n const emailsMatch = !emailProvided || email.trim() === email2.trim();\n const canProceedStep3 =\n acceptTerms && !!cancelMonth && confirmMonthEnd && emailValid && emailsMatch;\n const runDate = cancelMonth ? `${cancelMonth}01` : undefined;\n\n const submit = async () => {\n setLoading(true);\n setError(null);\n setMessage(null);\n try {\n await simActionsService.cancel(subscriptionId, { scheduledAt: runDate });\n setMessage(\"Cancellation request submitted. You will receive a confirmation email.\");\n setTimeout(() => router.push(`/subscriptions/${subscriptionId}#sim-management`), 1500);\n } catch (e: unknown) {\n setError(e instanceof Error ? e.message : \"Failed to submit cancellation\");\n } finally {\n setLoading(false);\n }\n };\n\n return (\n
\n
\n \n ← Back to SIM Management\n \n
Step {step} of 3
\n
\n\n {error && (\n
{error}
\n )}\n {message && (\n
\n {message}\n
\n )}\n\n
\n

Cancel SIM

\n

\n Cancel SIM: Permanently cancel your SIM service. This action cannot be undone and will\n terminate your service immediately.\n

\n\n {step === 1 && (\n
\n
\n \n \n
\n \n {\n setCancelMonth(e.target.value);\n setConfirmMonthEnd(false);\n }}\n className=\"w-full border border-gray-300 rounded-md px-3 py-2 text-sm\"\n >\n \n {monthOptions.map(opt => (\n \n ))}\n \n

\n Cancellation takes effect at the start of the selected month.\n

\n
\n
\n
\n setStep(2)}\n className=\"px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50\"\n >\n Next\n \n
\n
\n )}\n\n {step === 2 && (\n
\n
\n \n Online cancellations must be made from this website by the 25th of the desired\n cancellation month. Once a request of a cancellation of the SONIXNET SIM is accepted\n from this online form, a confirmation email containing details of the SIM plan will\n be sent to the registered email address. The SIM card is a rental piece of hardware\n and must be returned to Assist Solutions upon cancellation. The cancellation request\n through this website retains to your SIM subscriptions only. To cancel any other\n services with Assist Solutions (home internet etc.) please contact Assist Solutions\n at info@asolutions.co.jp\n \n \n The SONIXNET SIM has a minimum contract term agreement of three months (sign-up\n month is not included in the minimum term of three months; ie. sign-up in January =\n minimum term is February, March, April). If the minimum contract term is not\n fulfilled, the monthly fees of the remaining months will be charged upon\n cancellation.\n \n \n Cancellation of option services only (Voice Mail, Call Waiting) while keeping the\n base plan active is not possible from this online form. Please contact Assist\n Solutions Customer Support (info@asolutions.co.jp) for more information. Upon\n cancelling the base plan, all additional options associated with the requested SIM\n plan will be cancelled.\n \n \n Upon cancellation the SIM phone number will be lost. In order to keep the phone\n number active to be used with a different cellular provider, a request for an MNP\n transfer (administrative fee \\\\1,000yen+tax) is necessary. The MNP cannot be\n requested from this online form. Please contact Assist Solutions Customer Support\n (info@asolutions.co.jp) for more information.\n \n
\n
\n setAcceptTerms(e.target.checked)}\n />\n \n
\n
\n setConfirmMonthEnd(e.target.checked)}\n disabled={!cancelMonth}\n />\n \n
\n
\n setStep(1)}\n className=\"px-4 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50\"\n >\n Back\n \n setStep(3)}\n className=\"px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50\"\n >\n Next\n \n
\n
\n )}\n\n {step === 3 && (\n
\n {registeredEmail && (\n
\n Your registered email address is:{\" \"}\n {registeredEmail}\n
\n )}\n
\n You will receive a cancellation confirmation email. If you would like to receive this\n email on a different address, please enter the address below.\n
\n
\n
\n \n setEmail(e.target.value)}\n placeholder=\"you@example.com\"\n />\n
\n
\n \n setEmail2(e.target.value)}\n placeholder=\"you@example.com\"\n />\n
\n
\n \n setNotes(e.target.value)}\n placeholder=\"If you have any questions or requests, note them here.\"\n />\n
\n
\n {emailProvided && !emailValid && (\n
\n Please enter a valid email address in both fields.\n
\n )}\n {emailProvided && emailValid && !emailsMatch && (\n
Email addresses do not match.
\n )}\n
\n Your cancellation request is not confirmed yet. This is the final page. To finalize\n your cancellation request please proceed from REQUEST CANCELLATION below.\n
\n
\n setStep(2)}\n className=\"px-4 py-2 rounded-md border border-gray-300 text-sm text-gray-700 bg-white hover:bg-gray-50\"\n >\n Back\n \n {\n if (\n window.confirm(\n \"Request cancellation now? This will schedule the cancellation for \" +\n (runDate || \"\") +\n \".\"\n )\n ) {\n void submit();\n }\n }}\n disabled={loading || !runDate || !canProceedStep3}\n className=\"px-4 py-2 rounded-md bg-red-600 text-white text-sm disabled:opacity-50\"\n >\n {loading ? \"Processing…\" : \"Request Cancellation\"}\n \n
\n
\n )}\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/containers/SubscriptionDetail.tsx","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Replace `\"use·client\"` with `(\"use·client\")`","line":2,"column":1,"nodeType":null,"messageId":"replace","endLine":2,"endColumn":13,"fix":{"range":[66,78],"text":"(\"use client\")"}},{"ruleId":"@typescript-eslint/no-unused-expressions","severity":1,"message":"Expected an assignment or function call and instead saw an expression.","line":2,"column":1,"nodeType":"ExpressionStatement","messageId":"unusedExpression","endLine":2,"endColumn":14},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'ArrowTopRightOnSquareIcon' is defined but never used.","line":19,"column":3,"nodeType":null,"messageId":"unusedVar","endLine":19,"endColumn":28},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'currentPage' is assigned a value but never used.","line":30,"column":10,"nodeType":null,"messageId":"unusedVar","endLine":30,"endColumn":21},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'setCurrentPage' is assigned a value but never used.","line":30,"column":23,"nodeType":null,"messageId":"unusedVar","endLine":30,"endColumn":37},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'itemsPerPage' is assigned a value but never used.","line":31,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":31,"endColumn":21},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'getStatusColor' is assigned a value but never used.","line":77,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":77,"endColumn":23},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'getInvoiceStatusIcon' is assigned a value but never used.","line":94,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":94,"endColumn":29},{"ruleId":"@typescript-eslint/no-unused-vars","severity":1,"message":"'getInvoiceStatusColor' is assigned a value but never used.","line":107,"column":9,"nodeType":null,"messageId":"unusedVar","endLine":107,"endColumn":30},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `··`","line":220,"column":19,"nodeType":null,"messageId":"insert","endLine":220,"endColumn":19,"fix":{"range":[7468,7468],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `··`","line":221,"column":1,"nodeType":null,"messageId":"insert","endLine":221,"endColumn":1,"fix":{"range":[7480,7480],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `····`","line":222,"column":19,"nodeType":null,"messageId":"insert","endLine":222,"endColumn":19,"fix":{"range":[7576,7576],"text":" "}},{"ruleId":"prettier/prettier","severity":1,"message":"Insert `····`","line":223,"column":1,"nodeType":null,"messageId":"insert","endLine":223,"endColumn":1,"fix":{"range":[7588,7588],"text":" "}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":13,"fixableErrorCount":0,"fixableWarningCount":5,"source":"import { LoadingSpinner } from \"@/components/ui/loading-spinner\";\n\"use client\";\n\nimport { SubCard } from \"@/components/ui/sub-card\";\nimport { DetailHeader } from \"@/components/common/DetailHeader\";\n\nimport { useEffect, useState } from \"react\";\nimport { useParams, useSearchParams } from \"next/navigation\";\nimport Link from \"next/link\";\nimport {\n ArrowLeftIcon,\n ServerIcon,\n CheckCircleIcon,\n ExclamationTriangleIcon,\n ClockIcon,\n XCircleIcon,\n CalendarIcon,\n DocumentTextIcon,\n ArrowTopRightOnSquareIcon,\n} from \"@heroicons/react/24/outline\";\nimport { format } from \"date-fns\";\nimport { useSubscription } from \"@/features/subscriptions/hooks\";\nimport { InvoicesList } from \"@/features/billing/components/InvoiceList/InvoiceList\";\nimport { formatCurrency as sharedFormatCurrency, getCurrencyLocale } from \"@/utils/currency\";\nimport { SimManagementSection } from \"@/features/sim-management\";\n\nexport function SubscriptionDetailContainer() {\n const params = useParams();\n const searchParams = useSearchParams();\n const [currentPage, setCurrentPage] = useState(1);\n const itemsPerPage = 10;\n const [showInvoices, setShowInvoices] = useState(true);\n const [showSimManagement, setShowSimManagement] = useState(false);\n\n const subscriptionId = parseInt(params.id as string);\n const { data: subscription, isLoading, error } = useSubscription(subscriptionId);\n // Invoices are now rendered via shared InvoiceList\n\n useEffect(() => {\n const updateVisibility = () => {\n const hash = typeof window !== \"undefined\" ? window.location.hash : \"\";\n const service = (searchParams.get(\"service\") || \"\").toLowerCase();\n const isSimContext = hash.includes(\"sim-management\") || service === \"sim\";\n if (isSimContext) {\n setShowInvoices(false);\n setShowSimManagement(true);\n } else {\n setShowInvoices(true);\n setShowSimManagement(false);\n }\n };\n updateVisibility();\n if (typeof window !== \"undefined\") {\n window.addEventListener(\"hashchange\", updateVisibility);\n return () => window.removeEventListener(\"hashchange\", updateVisibility);\n }\n return;\n }, [searchParams]);\n\n const getStatusIcon = (status: string) => {\n switch (status) {\n case \"Active\":\n return ;\n case \"Suspended\":\n return ;\n case \"Terminated\":\n return ;\n case \"Cancelled\":\n return ;\n case \"Pending\":\n return ;\n default:\n return ;\n }\n };\n\n const getStatusColor = (status: string) => {\n switch (status) {\n case \"Active\":\n return \"bg-green-100 text-green-800\";\n case \"Suspended\":\n return \"bg-yellow-100 text-yellow-800\";\n case \"Terminated\":\n return \"bg-red-100 text-red-800\";\n case \"Cancelled\":\n return \"bg-gray-100 text-gray-800\";\n case \"Pending\":\n return \"bg-blue-100 text-blue-800\";\n default:\n return \"bg-gray-100 text-gray-800\";\n }\n };\n\n const getInvoiceStatusIcon = (status: string) => {\n switch (status) {\n case \"Paid\":\n return ;\n case \"Overdue\":\n return ;\n case \"Unpaid\":\n return ;\n default:\n return ;\n }\n };\n\n const getInvoiceStatusColor = (status: string) => {\n switch (status) {\n case \"Paid\":\n return \"bg-green-100 text-green-800\";\n case \"Overdue\":\n return \"bg-red-100 text-red-800\";\n case \"Unpaid\":\n return \"bg-yellow-100 text-yellow-800\";\n case \"Cancelled\":\n return \"bg-gray-100 text-gray-800\";\n default:\n return \"bg-gray-100 text-gray-800\";\n }\n };\n\n const formatDate = (dateString: string | undefined) => {\n if (!dateString) return \"N/A\";\n try {\n return format(new Date(dateString), \"MMM d, yyyy\");\n } catch {\n return \"Invalid date\";\n }\n };\n\n const formatCurrency = (amount: number) =>\n sharedFormatCurrency(amount || 0, { currency: \"JPY\", locale: getCurrencyLocale(\"JPY\") });\n\n const formatBillingLabel = (cycle: string) => {\n switch (cycle) {\n case \"Monthly\":\n return \"Monthly Billing\";\n case \"Annually\":\n return \"Annual Billing\";\n case \"Quarterly\":\n return \"Quarterly Billing\";\n case \"Semi-Annually\":\n return \"Semi-Annual Billing\";\n case \"Biennially\":\n return \"Biennial Billing\";\n case \"Triennially\":\n return \"Triennial Billing\";\n default:\n return \"One-time Payment\";\n }\n };\n\n if (isLoading) {\n return (\n
\n
\n \n

Loading subscription...

\n
\n
\n );\n }\n\n if (error || !subscription) {\n return (\n
\n
\n
\n
\n \n
\n
\n

Error loading subscription

\n
\n {error instanceof Error ? error.message : \"Subscription not found\"}\n
\n
\n \n ← Back to subscriptions\n \n
\n
\n
\n
\n
\n );\n }\n\n return (\n
\n
\n
\n
\n
\n \n \n \n
\n \n
\n

{subscription.productName}

\n

Service ID: {subscription.serviceId}

\n
\n
\n
\n
\n
\n\n \n \n
\n
\n
\n

\n Billing Amount\n

\n

\n {formatCurrency(subscription.amount)}\n

\n

{formatBillingLabel(subscription.cycle)}

\n
\n
\n

\n Next Due Date\n

\n

{formatDate(subscription.nextDue)}

\n
\n \n Due date\n
\n
\n
\n

\n Registration Date\n

\n

\n {formatDate(subscription.registrationDate)}\n

\n Service created\n
\n
\n
\n
\n\n {subscription.productName.toLowerCase().includes(\"sim\") && (\n
\n \n
\n
\n

Service Management

\n

\n Switch between billing and SIM management views\n

\n
\n
\n \n \n SIM Management\n \n \n \n Invoices\n \n
\n
\n
\n
\n )}\n\n {showSimManagement && (\n
\n \n
\n )}\n\n {showInvoices && }\n
\n
\n );\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/containers/SubscriptionsList.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/hooks/useSubscriptions.ts","messages":[{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":172,"column":7,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":172,"endColumn":70,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[4770,4770],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[4770,4770],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":173,"column":7,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":173,"endColumn":73,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[4840,4840],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[4840,4840],"text":"await "},"desc":"Add await operator."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Subscriptions Hooks\n * React hooks for subscription functionality using shared types\n */\n\nimport { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { useAuthStore } from \"@/lib/auth/store\";\nimport { apiClient } from \"@/lib/api/client\";\nimport type { Subscription, SubscriptionList, InvoiceList } from \"@customer-portal/shared\";\n\ninterface UseSubscriptionsOptions {\n status?: string;\n}\n\n/**\n * Hook to fetch all subscriptions\n */\nexport function useSubscriptions(options: UseSubscriptionsOptions = {}) {\n const { status } = options;\n const { token, isAuthenticated } = useAuthStore();\n\n return useQuery({\n queryKey: [\"subscriptions\", status],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const params = new URLSearchParams({\n ...(status && { status }),\n });\n const res = await apiClient.get(\n `/subscriptions?${params}`\n );\n return res.data as SubscriptionList | Subscription[];\n },\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n\n/**\n * Hook to fetch active subscriptions only\n */\nexport function useActiveSubscriptions() {\n const { token, isAuthenticated } = useAuthStore();\n\n return useQuery({\n queryKey: [\"subscriptions\", \"active\"],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const res = await apiClient.get(`/subscriptions/active`);\n return res.data as Subscription[];\n },\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n\n/**\n * Hook to fetch subscription statistics\n */\nexport function useSubscriptionStats() {\n const { token, isAuthenticated } = useAuthStore();\n\n return useQuery<{\n total: number;\n active: number;\n suspended: number;\n cancelled: number;\n pending: number;\n }>({\n queryKey: [\"subscriptions\", \"stats\"],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const res = await apiClient.get<{\n total: number;\n active: number;\n suspended: number;\n cancelled: number;\n pending: number;\n }>(`/subscriptions/stats`);\n return res.data as {\n total: number;\n active: number;\n suspended: number;\n cancelled: number;\n pending: number;\n };\n },\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n\n/**\n * Hook to fetch a specific subscription\n */\nexport function useSubscription(subscriptionId: number) {\n const { token, isAuthenticated } = useAuthStore();\n\n return useQuery({\n queryKey: [\"subscription\", subscriptionId],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const res = await apiClient.get(`/subscriptions/${subscriptionId}`);\n return res.data as Subscription;\n },\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n enabled: isAuthenticated && !!token,\n });\n}\n\n/**\n * Hook to fetch subscription invoices\n */\nexport function useSubscriptionInvoices(\n subscriptionId: number,\n options: { page?: number; limit?: number } = {}\n) {\n const { page = 1, limit = 10 } = options;\n const { token, isAuthenticated } = useAuthStore();\n\n return useQuery({\n queryKey: [\"subscription-invoices\", subscriptionId, page, limit],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const params = new URLSearchParams({\n page: page.toString(),\n limit: limit.toString(),\n });\n const res = await apiClient.get(\n `/subscriptions/${subscriptionId}/invoices?${params}`\n );\n return res.data as InvoiceList;\n },\n staleTime: 60 * 1000, // 1 minute\n gcTime: 5 * 60 * 1000, // 5 minutes\n enabled: isAuthenticated && !!token && !!subscriptionId,\n });\n}\n\n/**\n * Hook to perform subscription actions (suspend, resume, cancel, etc.)\n */\nexport function useSubscriptionAction() {\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async ({ id, action }: { id: number; action: string }) => {\n const res = await apiClient.post(`/subscriptions/${id}/actions`, { action });\n return res.data;\n },\n onSuccess: (_, { id }) => {\n // Invalidate relevant queries after successful action\n queryClient.invalidateQueries({ queryKey: [\"subscriptions\"] });\n queryClient.invalidateQueries({ queryKey: [\"subscription\", id] });\n },\n });\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/services/sim-actions.service.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":6,"column":37,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":6,"endColumn":40,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[130,133],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[130,133],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":6,"column":51,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":6,"endColumn":54,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[144,147],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[144,147],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":12,"column":31,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":12,"endColumn":34,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[255,258],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[255,258],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":12,"column":45,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":12,"endColumn":48,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[269,272],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[269,272],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * SIM Actions Service (feature layer)\n */\nimport { apiClient } from \"@/lib/api/client\";\n\nexport interface SimInfo {\n details: TDetails;\n usage: TUsage;\n}\n\nexport class SimActionsService {\n async getSimInfo(\n subscriptionId: number\n ): Promise> {\n const res = await apiClient.get>(\n `/subscriptions/${subscriptionId}/sim`\n );\n return res.data as SimInfo;\n }\n\n async changePlan(\n subscriptionId: number,\n body: { newPlanCode: string; assignGlobalIp?: boolean; scheduledAt?: string }\n ) {\n const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/change-plan`, body);\n return res.data;\n }\n\n async topUp(subscriptionId: number, body: { quotaMb: number; scheduledAt?: string }) {\n const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/top-up`, body);\n return res.data;\n }\n\n async cancel(subscriptionId: number, body: { scheduledAt?: string } = {}) {\n const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/cancel`, body);\n return res.data;\n }\n\n async reissueEsim(subscriptionId: number) {\n const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/reissue-esim`);\n return res.data;\n }\n\n async updateFeatures(\n subscriptionId: number,\n payload: {\n voiceMailEnabled?: boolean;\n callWaitingEnabled?: boolean;\n internationalRoamingEnabled?: boolean;\n networkType?: \"4G\" | \"5G\";\n }\n ) {\n const res = await apiClient.post(`/subscriptions/${subscriptionId}/sim/features`, payload);\n return res.data;\n }\n}\n\nexport const simActionsService = new SimActionsService();\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/subscriptions/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/features/support/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/hooks/use-optimized-query.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":47,"column":15,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":47,"endColumn":18,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1179,1182],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1179,1182],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":48,"column":28,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":48,"endColumn":31,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1213,1216],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1213,1216],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":56,"column":9,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":60,"endColumn":12,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[1489,1489],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[1489,1489],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-floating-promises","severity":2,"message":"Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator.","line":65,"column":9,"nodeType":"ExpressionStatement","messageId":"floatingVoid","endLine":69,"endColumn":12,"suggestions":[{"messageId":"floatingFixVoid","fix":{"range":[1704,1704],"text":"void "},"desc":"Add void operator to ignore."},{"messageId":"floatingFixAwait","fix":{"range":[1704,1704],"text":"await "},"desc":"Add await operator."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":81,"column":13,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":81,"endColumn":16,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1961,1964],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1961,1964],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":99,"column":54,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":99,"endColumn":57,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2399,2402],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2399,2402],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":6,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import {\n useQuery,\n useInfiniteQuery,\n useQueryClient,\n UseQueryOptions,\n UseInfiniteQueryOptions,\n} from \"@tanstack/react-query\";\nimport { queryConfigs, queryClient } from \"@/lib/query-client\";\n\ntype DataType = \"static\" | \"profile\" | \"financial\" | \"realtime\" | \"list\";\n\n/**\n * Optimized query hook with predefined configurations for different data types\n */\nexport function useOptimizedQuery(\n options: UseQueryOptions & { dataType?: DataType }\n) {\n const { dataType = \"list\", ...queryOptions } = options;\n const config = queryConfigs[dataType];\n\n return useQuery({\n ...config,\n ...queryOptions,\n });\n}\n\n/**\n * Optimized infinite query hook\n */\nexport function useOptimizedInfiniteQuery(\n options: UseInfiniteQueryOptions & { dataType?: DataType }\n) {\n const { dataType = \"list\", ...queryOptions } = options;\n const config = queryConfigs[dataType];\n\n return useInfiniteQuery({\n ...config,\n ...queryOptions,\n });\n}\n\n/**\n * Hook for prefetching queries with optimized timing\n */\nexport function usePrefetchQuery() {\n const prefetchQuery = (\n queryKey: any[],\n queryFn: () => Promise,\n dataType: DataType = \"list\"\n ) => {\n const config = queryConfigs[dataType];\n\n // Use requestIdleCallback for non-critical prefetching\n if (typeof window !== \"undefined\" && \"requestIdleCallback\" in window) {\n window.requestIdleCallback(() => {\n queryClient.prefetchQuery({\n queryKey,\n queryFn,\n ...config,\n });\n });\n } else {\n // Fallback for browsers without requestIdleCallback\n setTimeout(() => {\n queryClient.prefetchQuery({\n queryKey,\n queryFn,\n ...config,\n });\n }, 100);\n }\n };\n\n return { prefetchQuery };\n}\n\n/**\n * Hook for background data synchronization\n */\nexport function useBackgroundSync(\n queryKey: any[],\n enabled: boolean = true,\n interval: number = 5 * 60 * 1000 // 5 minutes\n) {\n return useQuery({\n queryKey,\n enabled,\n refetchInterval: interval,\n refetchIntervalInBackground: false,\n refetchOnWindowFocus: true,\n refetchOnReconnect: true,\n staleTime: interval / 2, // Half of refetch interval\n });\n}\n\n/**\n * Hook for optimistic updates with rollback\n */\nexport function useOptimisticUpdate(queryKey: any[]) {\n const queryClient = useQueryClient();\n\n const updateOptimistically = async (\n variables: TVariables,\n updater: (oldData: TData | undefined, variables: TVariables) => TData,\n mutationFn: (variables: TVariables) => Promise\n ) => {\n // Cancel outgoing refetches\n await queryClient.cancelQueries({ queryKey });\n\n // Snapshot previous value\n const previousData = queryClient.getQueryData(queryKey);\n\n // Optimistically update\n queryClient.setQueryData(queryKey, old => updater(old, variables));\n\n try {\n // Perform the actual mutation\n const result = await mutationFn(variables);\n\n // Update with real data\n queryClient.setQueryData(queryKey, result);\n\n return result;\n } catch (error) {\n // Rollback on error\n queryClient.setQueryData(queryKey, previousData);\n throw error;\n }\n };\n\n return { updateOptimistically };\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/hooks/use-performance-monitor.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api/base.service.ts","messages":[{"ruleId":"@typescript-eslint/no-base-to-string","severity":2,"message":"'value' will use Object's default stringification format ('[object Object]') when stringified.","line":46,"column":37,"nodeType":"Identifier","messageId":"baseToString","endLine":46,"endColumn":42}],"suppressedMessages":[],"errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Base Service Class\n * Provides common CRUD operations and utilities for domain-specific services\n */\n\nimport type { ApiClient, ApiResponse } from \"./client\";\nimport type { CrudService } from \"../types/api.types\";\nimport type { PaginatedResponse, QueryParams } from \"../types/common.types\";\n\nexport abstract class BaseService, UpdateT = Partial>\n implements CrudService\n{\n protected abstract readonly basePath: string;\n\n constructor(protected readonly apiClient: ApiClient) {}\n\n /**\n * Build endpoint path\n */\n protected buildPath(path?: string): string {\n if (!path) return this.basePath;\n return `${this.basePath}/${path}`;\n }\n\n /**\n * Transform query parameters for API request\n */\n protected transformParams(params?: QueryParams): Record {\n if (!params) return {};\n\n const transformed: Record = {};\n\n Object.entries(params).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n if (Array.isArray(value)) {\n // Convert arrays to comma-separated strings\n transformed[key] = value.join(\",\");\n } else if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n transformed[key] = value;\n } else {\n // Convert other types to string\n transformed[key] = String(value);\n }\n }\n });\n\n return transformed;\n }\n\n /**\n * Extract data from API response\n */\n protected extractData(response: ApiResponse): R {\n if (!response.success || response.data === undefined) {\n throw new Error(\"Invalid API response\");\n }\n return response.data;\n }\n\n /**\n * Get all items with optional pagination and filtering\n */\n async getAll(params?: QueryParams): Promise> {\n const response = await this.apiClient.get>(this.basePath, {\n params: this.transformParams(params),\n });\n return this.extractData(response);\n }\n\n /**\n * Get single item by ID\n */\n async getById(id: string): Promise {\n const response = await this.apiClient.get(this.buildPath(id));\n return this.extractData(response);\n }\n\n /**\n * Create new item\n */\n async create(data: CreateT): Promise {\n const response = await this.apiClient.post(this.basePath, data);\n return this.extractData(response);\n }\n\n /**\n * Update existing item\n */\n async update(id: string, data: UpdateT): Promise {\n const response = await this.apiClient.patch(this.buildPath(id), data);\n return this.extractData(response);\n }\n\n /**\n * Delete item\n */\n async delete(id: string): Promise {\n await this.apiClient.delete(this.buildPath(id));\n }\n\n /**\n * Check if item exists\n */\n async exists(id: string): Promise {\n try {\n await this.getById(id);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Get items by IDs\n */\n async getByIds(ids: string[]): Promise {\n const response = await this.apiClient.get(this.basePath, {\n params: { ids: ids.join(\",\") },\n });\n return this.extractData(response);\n }\n\n /**\n * Bulk create items\n */\n async bulkCreate(items: CreateT[]): Promise {\n const response = await this.apiClient.post(this.buildPath(\"bulk\"), { items });\n return this.extractData(response);\n }\n\n /**\n * Bulk update items\n */\n async bulkUpdate(updates: Array<{ id: string; data: UpdateT }>): Promise {\n const response = await this.apiClient.patch(this.buildPath(\"bulk\"), { updates });\n return this.extractData(response);\n }\n\n /**\n * Bulk delete items\n */\n async bulkDelete(ids: string[]): Promise {\n await this.apiClient.delete(this.buildPath(\"bulk\"), {\n data: { ids },\n });\n }\n\n /**\n * Search items\n */\n async search(query: string, params?: QueryParams): Promise> {\n const searchParams = {\n q: query,\n ...params,\n };\n\n const response = await this.apiClient.get>(this.buildPath(\"search\"), {\n params: this.transformParams(searchParams),\n });\n return this.extractData(response);\n }\n\n /**\n * Count items with optional filtering\n */\n async count(params?: QueryParams): Promise {\n const response = await this.apiClient.get<{ count: number }>(this.buildPath(\"count\"), {\n params: this.transformParams(params),\n });\n const data = this.extractData(response);\n return (data as { count: number }).count;\n }\n}\n\n/**\n * Authenticated Base Service\n * Extends BaseService with authentication-aware methods\n */\nexport abstract class AuthenticatedBaseService<\n T,\n CreateT = Partial,\n UpdateT = Partial,\n> extends BaseService {\n /**\n * Get current user's items\n */\n async getMine(params?: QueryParams): Promise> {\n const response = await this.apiClient.get>(this.buildPath(\"mine\"), {\n params: this.transformParams(params),\n });\n return this.extractData(response);\n }\n\n /**\n * Get items for specific user (admin only)\n */\n async getByUserId(userId: string, params?: QueryParams): Promise> {\n const searchParams = {\n userId,\n ...params,\n };\n\n const response = await this.apiClient.get>(this.basePath, {\n params: this.transformParams(searchParams),\n });\n return this.extractData(response);\n }\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api/client.ts","messages":[{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async arrow function has no 'await' expression.","line":356,"column":46,"nodeType":"ArrowFunctionExpression","messageId":"missingAwait","endLine":356,"endColumn":48,"suggestions":[{"messageId":"removeAsync","fix":{"range":[9748,9754],"text":""},"desc":"Remove 'async'."}]},{"ruleId":"@typescript-eslint/require-await","severity":2,"message":"Async arrow function has no 'await' expression.","line":366,"column":49,"nodeType":"ArrowFunctionExpression","messageId":"missingAwait","endLine":366,"endColumn":51,"suggestions":[{"messageId":"removeAsync","fix":{"range":[10000,10006],"text":""},"desc":"Remove 'async'."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Centralized API Client\n * Provides consistent error handling, authentication, and request/response interceptors\n */\n\nimport { env } from \"../env\";\nimport { logger } from \"../logger\";\nimport type { ApiRequestConfig, RequestInterceptor, ResponseInterceptor } from \"../types/api.types\";\n\n// Local ApiResponse interface for the client\nexport interface ApiResponse {\n success: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n details?: Record;\n };\n meta?: {\n requestId?: string;\n timestamp?: string;\n };\n}\n\nexport class ApiError extends Error {\n constructor(\n message: string,\n public status: number,\n public code?: string,\n public details?: Record\n ) {\n super(message);\n this.name = \"ApiError\";\n }\n}\n\nexport interface ApiClientConfig {\n baseUrl: string;\n timeout?: number;\n retries?: number;\n defaultHeaders?: Record;\n}\n\nexport class ApiClient {\n private baseUrl: string;\n private timeout: number;\n private retries: number;\n private defaultHeaders: Record;\n private requestInterceptors: RequestInterceptor[] = [];\n private responseInterceptors: ResponseInterceptor[] = [];\n\n constructor(config: ApiClientConfig) {\n this.baseUrl = config.baseUrl.endsWith(\"/\") ? config.baseUrl.slice(0, -1) : config.baseUrl;\n this.timeout = config.timeout ?? 30000;\n this.retries = config.retries ?? 3;\n this.defaultHeaders = {\n \"Content-Type\": \"application/json\",\n ...config.defaultHeaders,\n };\n }\n\n /**\n * Add request interceptor\n */\n addRequestInterceptor(interceptor: RequestInterceptor): void {\n this.requestInterceptors.push(interceptor);\n }\n\n /**\n * Add response interceptor\n */\n addResponseInterceptor(interceptor: ResponseInterceptor): void {\n this.responseInterceptors.push(interceptor);\n }\n\n /**\n * Apply request interceptors\n */\n private async applyRequestInterceptors(config: ApiRequestConfig): Promise {\n let processedConfig = { ...config };\n\n for (const interceptor of this.requestInterceptors) {\n processedConfig = await interceptor(processedConfig);\n }\n\n return processedConfig;\n }\n\n /**\n * Apply response interceptors\n */\n private async applyResponseInterceptors(response: ApiResponse): Promise> {\n let processedResponse = { ...response };\n\n for (const interceptor of this.responseInterceptors) {\n processedResponse = await interceptor(processedResponse);\n }\n\n return processedResponse;\n }\n\n /**\n * Build full URL from endpoint\n */\n private buildUrl(endpoint: string, params?: Record): string {\n const path = endpoint.startsWith(\"/\") ? endpoint : `/${endpoint}`;\n const url = `${this.baseUrl}${path}`;\n\n if (!params || Object.keys(params).length === 0) {\n return url;\n }\n\n const searchParams = new URLSearchParams();\n Object.entries(params).forEach(([key, value]) => {\n searchParams.append(key, String(value));\n });\n\n return `${url}?${searchParams.toString()}`;\n }\n\n /**\n * Create AbortController with timeout\n */\n private createAbortController(timeout?: number): AbortController {\n const controller = new AbortController();\n const timeoutMs = timeout ?? this.timeout;\n\n setTimeout(() => {\n controller.abort();\n }, timeoutMs);\n\n return controller;\n }\n\n /**\n * Parse error response\n */\n private async parseErrorResponse(response: Response): Promise {\n let errorMessage = `HTTP ${response.status}`;\n let errorCode: string | undefined;\n let errorDetails: Record | undefined;\n\n try {\n const errorData = (await response.json()) as unknown;\n\n if (typeof errorData === \"object\" && errorData !== null) {\n const errorObj = errorData as Record;\n\n if (\"message\" in errorObj && typeof errorObj.message === \"string\") {\n errorMessage = errorObj.message;\n }\n\n if (\"code\" in errorObj && typeof errorObj.code === \"string\") {\n errorCode = errorObj.code;\n }\n\n if (\"details\" in errorObj && typeof errorObj.details === \"object\") {\n errorDetails = errorObj.details as Record;\n }\n }\n } catch {\n // If we can't parse the error response, use the status text\n errorMessage = response.statusText || errorMessage;\n }\n\n return new ApiError(errorMessage, response.status, errorCode, errorDetails);\n }\n\n /**\n * Make HTTP request with retries\n */\n private async makeRequest(\n endpoint: string,\n config: ApiRequestConfig = {}\n ): Promise> {\n // Apply request interceptors\n const processedConfig = await this.applyRequestInterceptors(config);\n\n const url = this.buildUrl(\n endpoint,\n processedConfig.params as Record\n );\n const method = processedConfig.method ?? \"GET\";\n\n // Merge headers\n const headers = {\n ...this.defaultHeaders,\n ...processedConfig.headers,\n };\n\n // Create request options\n const requestOptions: RequestInit = {\n method,\n headers,\n credentials: \"include\", // Include cookies for session management\n signal: this.createAbortController(processedConfig.timeout).signal,\n };\n\n // Add body for non-GET requests\n if (processedConfig.data && method !== \"GET\") {\n requestOptions.body =\n typeof processedConfig.data === \"string\"\n ? processedConfig.data\n : JSON.stringify(processedConfig.data);\n }\n\n let lastError: Error | null = null;\n const maxRetries = processedConfig.retries ?? this.retries;\n\n // Retry logic\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n logger.debug(`API Request: ${method} ${url}`, {\n attempt: attempt + 1,\n maxRetries: maxRetries + 1,\n });\n\n const response = await fetch(url, requestOptions);\n\n // Handle error responses\n if (!response.ok) {\n const apiError = await this.parseErrorResponse(response);\n\n // Don't retry client errors (4xx) except for 429 (rate limit)\n if (response.status >= 400 && response.status < 500 && response.status !== 429) {\n throw apiError;\n }\n\n // Retry server errors (5xx) and rate limits (429)\n if (attempt < maxRetries) {\n lastError = apiError;\n const delay = Math.pow(2, attempt) * 1000; // Exponential backoff\n await new Promise(resolve => setTimeout(resolve, delay));\n continue;\n }\n\n throw apiError;\n }\n\n // Parse successful response\n let data: T;\n\n if (response.status === 204) {\n // No content\n data = undefined as T;\n } else {\n const contentType = response.headers.get(\"content-type\");\n if (contentType?.includes(\"application/json\")) {\n data = (await response.json()) as T;\n } else {\n data = (await response.text()) as T;\n }\n }\n\n const apiResponse: ApiResponse = {\n success: true,\n data,\n meta: {\n requestId: response.headers.get(\"x-request-id\") || undefined,\n timestamp: new Date().toISOString(),\n },\n };\n\n // Apply response interceptors\n return await this.applyResponseInterceptors(apiResponse);\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n // Don't retry on abort (timeout) or network errors on last attempt\n if (attempt >= maxRetries) {\n break;\n }\n\n // Exponential backoff for retries\n const delay = Math.pow(2, attempt) * 1000;\n await new Promise(resolve => setTimeout(resolve, delay));\n }\n }\n\n // If we get here, all retries failed\n throw lastError ?? new ApiError(\"Request failed after retries\", 0);\n }\n\n /**\n * GET request\n */\n async get(endpoint: string, config?: ApiRequestConfig): Promise> {\n return this.makeRequest(endpoint, { ...config, method: \"GET\" });\n }\n\n /**\n * POST request\n */\n async post(\n endpoint: string,\n data?: unknown,\n config?: ApiRequestConfig\n ): Promise> {\n return this.makeRequest(endpoint, { ...config, method: \"POST\", data });\n }\n\n /**\n * PUT request\n */\n async put(\n endpoint: string,\n data?: unknown,\n config?: ApiRequestConfig\n ): Promise> {\n return this.makeRequest(endpoint, { ...config, method: \"PUT\", data });\n }\n\n /**\n * PATCH request\n */\n async patch(\n endpoint: string,\n data?: unknown,\n config?: ApiRequestConfig\n ): Promise> {\n return this.makeRequest(endpoint, { ...config, method: \"PATCH\", data });\n }\n\n /**\n * DELETE request\n */\n async delete(endpoint: string, config?: ApiRequestConfig): Promise> {\n return this.makeRequest(endpoint, { ...config, method: \"DELETE\" });\n }\n}\n\n// Create default API client instance\nexport const apiClient = new ApiClient({\n baseUrl: env.NEXT_PUBLIC_API_BASE,\n timeout: 30000,\n retries: 3,\n});\n\n// Authentication interceptor\napiClient.addRequestInterceptor(async config => {\n // Import auth store dynamically to avoid circular dependencies\n const { useAuthStore } = await import(\"../auth/store\");\n const { token } = useAuthStore.getState();\n\n if (token) {\n config.headers = {\n ...config.headers,\n Authorization: `Bearer ${token}`,\n };\n }\n\n return config;\n});\n\n// Logging interceptor\napiClient.addRequestInterceptor(async config => {\n logger.debug(\"API Request\", {\n method: config.method,\n url: config.params ? \"with params\" : \"no params\",\n hasData: !!config.data,\n });\n return config;\n});\n\n// Response logging interceptor\napiClient.addResponseInterceptor(async response => {\n logger.debug(\"API Response\", {\n success: response.success,\n hasData: !!response.data,\n requestId: response.meta?.requestId,\n });\n return response;\n});\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api/services/auth.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api/services/billing.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/api/services/subscription.service.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/auth/api.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/auth/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/auth/store.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/design-system.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/env.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/form-validation.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":2,"column":32,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":2,"endColumn":35,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[60,63],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[60,63],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":14,"column":66,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":14,"endColumn":69,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[329,332],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[329,332],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\+.","line":48,"column":29,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":48,"endColumn":30,"suggestions":[{"messageId":"removeEscape","fix":{"range":[1501,1502],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[1501,1501],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]},{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\(.","line":49,"column":60,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":49,"endColumn":61,"suggestions":[{"messageId":"removeEscape","fix":{"range":[1583,1584],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[1583,1583],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]},{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\).","line":49,"column":62,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":49,"endColumn":63,"suggestions":[{"messageId":"removeEscape","fix":{"range":[1585,1586],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[1585,1585],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":104,"column":55,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":104,"endColumn":58,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2997,3000],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2997,3000],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":123,"column":54,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":123,"endColumn":57,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3504,3507],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3504,3507],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":130,"column":56,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":130,"endColumn":59,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3743,3746],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3743,3746],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":8,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// Form validation utilities\nexport type ValidationRule = {\n validate: (value: T) => boolean;\n message: string;\n};\n\nexport type ValidationResult = {\n isValid: boolean;\n errors: string[];\n};\n\n// Common validation rules\nexport const validationRules = {\n required: (message = \"This field is required\"): ValidationRule => ({\n validate: value => {\n if (typeof value === \"string\") return value.trim().length > 0;\n if (Array.isArray(value)) return value.length > 0;\n return value != null && value !== \"\";\n },\n message,\n }),\n\n email: (message = \"Please enter a valid email address\"): ValidationRule => ({\n validate: value => {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return !value || emailRegex.test(value);\n },\n message,\n }),\n\n minLength: (min: number, message?: string): ValidationRule => ({\n validate: value => !value || value.length >= min,\n message: message || `Must be at least ${min} characters`,\n }),\n\n maxLength: (max: number, message?: string): ValidationRule => ({\n validate: value => !value || value.length <= max,\n message: message || `Must be no more than ${max} characters`,\n }),\n\n pattern: (regex: RegExp, message = \"Invalid format\"): ValidationRule => ({\n validate: value => !value || regex.test(value),\n message,\n }),\n\n phone: (message = \"Please enter a valid phone number\"): ValidationRule => ({\n validate: value => {\n const phoneRegex = /^[\\+]?[1-9][\\d]{0,15}$/;\n return !value || phoneRegex.test(value.replace(/[\\s\\-\\(\\)]/g, \"\"));\n },\n message,\n }),\n\n url: (message = \"Please enter a valid URL\"): ValidationRule => ({\n validate: value => {\n try {\n return !value || Boolean(new URL(value));\n } catch {\n return false;\n }\n },\n message,\n }),\n\n number: (message = \"Please enter a valid number\"): ValidationRule => ({\n validate: value => !value || !isNaN(Number(value)),\n message,\n }),\n\n min: (min: number, message?: string): ValidationRule => ({\n validate: value => {\n const num = typeof value === \"string\" ? Number(value) : value;\n return !value || num >= min;\n },\n message: message || `Must be at least ${min}`,\n }),\n\n max: (max: number, message?: string): ValidationRule => ({\n validate: value => {\n const num = typeof value === \"string\" ? Number(value) : value;\n return !value || num <= max;\n },\n message: message || `Must be no more than ${max}`,\n }),\n};\n\n// Validate a single field against multiple rules\nexport function validateField(value: T, rules: ValidationRule[]): ValidationResult {\n const errors: string[] = [];\n\n for (const rule of rules) {\n if (!rule.validate(value)) {\n errors.push(rule.message);\n }\n }\n\n return {\n isValid: errors.length === 0,\n errors,\n };\n}\n\n// Validate multiple fields\nexport function validateForm>(\n values: T,\n rules: Partial[]>>\n): Record {\n const results = {} as Record;\n\n for (const [field, fieldRules] of Object.entries(rules) as [\n keyof T,\n ValidationRule[],\n ][]) {\n if (fieldRules) {\n results[field] = validateField(values[field], fieldRules);\n }\n }\n\n return results;\n}\n\n// Check if entire form is valid\nexport function isFormValid>(\n validationResults: Record\n): boolean {\n return Object.values(validationResults).every(result => result.isValid);\n}\n\n// Get first error for a field\nexport function getFieldError>(\n validationResults: Record,\n field: keyof T\n): string | undefined {\n return validationResults[field]?.errors[0];\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/hooks/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/logger.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/plan.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/query-client.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":64,"column":26,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":64,"endColumn":29,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1966,1969],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1966,1969],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":70,"column":25,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":70,"endColumn":28,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2129,2132],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2129,2132],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":72,"column":25,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":72,"endColumn":28,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2299,2302],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2299,2302],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":78,"column":21,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":78,"endColumn":24,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2472,2475],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2472,2475],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":86,"column":39,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":86,"endColumn":42,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2816,2819],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2816,2819],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":94,"column":22,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":94,"endColumn":25,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3066,3069],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3066,3069],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":6,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"// TanStack Query client configuration\n\nimport { QueryClient } from \"@tanstack/react-query\";\n\n// Performance-optimized query client configuration\nexport const queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n // Optimized stale times based on data type\n staleTime: 5 * 60 * 1000, // 5 minutes default\n gcTime: 10 * 60 * 1000, // 10 minutes garbage collection\n\n // Retry configuration with exponential backoff\n retry: (failureCount, error) => {\n // Don't retry on 4xx errors (client errors)\n if (error instanceof Error && \"status\" in error) {\n const status = error.status as number;\n if (status >= 400 && status < 500) {\n return false;\n }\n }\n // Retry up to 3 times with exponential backoff\n return failureCount < 3;\n },\n\n // Retry delay with exponential backoff\n retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),\n\n // Network mode for better offline handling\n networkMode: \"online\",\n\n // Refetch configuration\n refetchOnWindowFocus: false, // Disable aggressive refetching\n refetchOnReconnect: true,\n refetchOnMount: true,\n\n // Background refetch interval (disabled by default for performance)\n refetchInterval: false,\n refetchIntervalInBackground: false,\n },\n mutations: {\n // Don't retry mutations by default\n retry: false,\n\n // Network mode for mutations\n networkMode: \"online\",\n },\n },\n});\n\n// Query key factories for consistent caching\nexport const queryKeys = {\n // User-related queries\n user: {\n all: [\"user\"] as const,\n profile: () => [...queryKeys.user.all, \"profile\"] as const,\n preferences: () => [...queryKeys.user.all, \"preferences\"] as const,\n },\n\n // Dashboard queries\n dashboard: {\n all: [\"dashboard\"] as const,\n summary: () => [...queryKeys.dashboard.all, \"summary\"] as const,\n activity: (filters?: any) => [...queryKeys.dashboard.all, \"activity\", filters] as const,\n },\n\n // Billing queries\n billing: {\n all: [\"billing\"] as const,\n invoices: (params?: any) => [...queryKeys.billing.all, \"invoices\", params] as const,\n invoice: (id: string) => [...queryKeys.billing.all, \"invoice\", id] as const,\n payments: (params?: any) => [...queryKeys.billing.all, \"payments\", params] as const,\n },\n\n // Subscription queries\n subscriptions: {\n all: [\"subscriptions\"] as const,\n list: (params?: any) => [...queryKeys.subscriptions.all, \"list\", params] as const,\n detail: (id: string) => [...queryKeys.subscriptions.all, \"detail\", id] as const,\n usage: (id: string) => [...queryKeys.subscriptions.all, \"usage\", id] as const,\n },\n\n // Catalog queries\n catalog: {\n all: [\"catalog\"] as const,\n products: (type: string, params?: any) =>\n [...queryKeys.catalog.all, \"products\", type, params] as const,\n product: (id: string) => [...queryKeys.catalog.all, \"product\", id] as const,\n },\n\n // Support queries\n support: {\n all: [\"support\"] as const,\n cases: (params?: any) => [...queryKeys.support.all, \"cases\", params] as const,\n case: (id: string) => [...queryKeys.support.all, \"case\", id] as const,\n },\n} as const;\n\n// Optimized query configurations for different data types\nexport const queryConfigs = {\n // Static/rarely changing data (longer cache)\n static: {\n staleTime: 30 * 60 * 1000, // 30 minutes\n gcTime: 60 * 60 * 1000, // 1 hour\n },\n\n // User profile data (medium cache)\n profile: {\n staleTime: 10 * 60 * 1000, // 10 minutes\n gcTime: 30 * 60 * 1000, // 30 minutes\n },\n\n // Financial data (shorter cache for accuracy)\n financial: {\n staleTime: 2 * 60 * 1000, // 2 minutes\n gcTime: 10 * 60 * 1000, // 10 minutes\n },\n\n // Real-time data (very short cache)\n realtime: {\n staleTime: 30 * 1000, // 30 seconds\n gcTime: 2 * 60 * 1000, // 2 minutes\n },\n\n // List data (medium cache with background updates)\n list: {\n staleTime: 5 * 60 * 1000, // 5 minutes\n gcTime: 15 * 60 * 1000, // 15 minutes\n refetchOnWindowFocus: true,\n },\n} as const;\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/stores/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/api.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/auth.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/billing.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/catalog.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/common.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/form.types.ts","messages":[{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\(.","line":281,"column":38,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":281,"endColumn":39,"suggestions":[{"messageId":"removeEscape","fix":{"range":[7570,7571],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[7570,7570],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]},{"ruleId":"no-useless-escape","severity":2,"message":"Unnecessary escape character: \\).","line":281,"column":40,"nodeType":"Literal","messageId":"unnecessaryEscape","endLine":281,"endColumn":41,"suggestions":[{"messageId":"removeEscape","fix":{"range":[7572,7573],"text":""},"desc":"Remove the `\\`. This maintains the current functionality."},{"messageId":"escapeBackslash","fix":{"range":[7572,7572],"text":"\\"},"desc":"Replace the `\\` with `\\\\` to include the actual backslash character."}]}],"suppressedMessages":[],"errorCount":2,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Form validation and input types\n */\n\n// Base form types\nexport interface FormField {\n name: string;\n value: T;\n error?: string;\n touched: boolean;\n dirty: boolean;\n disabled?: boolean;\n required?: boolean;\n}\n\nexport interface FormState = Record> {\n values: T;\n errors: Partial>;\n touched: Partial>;\n dirty: boolean;\n valid: boolean;\n submitting: boolean;\n submitted: boolean;\n}\n\n// Validation types\nexport type ValidationRule = (value: T) => string | undefined;\n\nexport interface FieldValidation {\n required?: boolean | string;\n min?: number | string;\n max?: number | string;\n minLength?: number | string;\n maxLength?: number | string;\n pattern?: RegExp | string;\n custom?: ValidationRule[];\n}\n\nexport interface FormValidation = Record> {\n fields: Partial>;\n global?: ValidationRule[];\n}\n\n// Input component types\nexport interface BaseInputProps {\n name: string;\n label?: string;\n placeholder?: string;\n disabled?: boolean;\n required?: boolean;\n error?: string;\n helperText?: string;\n className?: string;\n testId?: string;\n}\n\nexport interface TextInputProps extends BaseInputProps {\n type?: \"text\" | \"email\" | \"password\" | \"tel\" | \"url\" | \"search\";\n value: string;\n onChange: (value: string) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n autoComplete?: string;\n maxLength?: number;\n minLength?: number;\n pattern?: string;\n readOnly?: boolean;\n autoFocus?: boolean;\n}\n\nexport interface NumberInputProps extends BaseInputProps {\n value: number | \"\";\n onChange: (value: number | \"\") => void;\n onBlur?: () => void;\n onFocus?: () => void;\n min?: number;\n max?: number;\n step?: number;\n precision?: number;\n format?: \"decimal\" | \"currency\" | \"percentage\";\n currency?: string;\n}\n\nexport interface SelectInputProps extends BaseInputProps {\n value: string | string[];\n onChange: (value: string | string[]) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n options: SelectOption[];\n multiple?: boolean;\n searchable?: boolean;\n clearable?: boolean;\n loading?: boolean;\n onSearch?: (query: string) => void;\n}\n\nexport interface SelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n group?: string;\n icon?: string;\n description?: string;\n}\n\nexport interface CheckboxInputProps extends BaseInputProps {\n checked: boolean;\n onChange: (checked: boolean) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n indeterminate?: boolean;\n}\n\nexport interface RadioInputProps extends BaseInputProps {\n value: string;\n selectedValue: string;\n onChange: (value: string) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n}\n\nexport interface TextareaInputProps extends BaseInputProps {\n value: string;\n onChange: (value: string) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n rows?: number;\n cols?: number;\n maxLength?: number;\n minLength?: number;\n resize?: \"none\" | \"vertical\" | \"horizontal\" | \"both\";\n autoResize?: boolean;\n}\n\nexport interface FileInputProps extends BaseInputProps {\n value: File[];\n onChange: (files: File[]) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n accept?: string;\n multiple?: boolean;\n maxSize?: number;\n maxFiles?: number;\n preview?: boolean;\n dragAndDrop?: boolean;\n}\n\nexport interface DateInputProps extends BaseInputProps {\n value: string;\n onChange: (value: string) => void;\n onBlur?: () => void;\n onFocus?: () => void;\n min?: string;\n max?: string;\n format?: string;\n showTime?: boolean;\n timezone?: string;\n}\n\n// Form component types\nexport interface FormProps = Record> {\n initialValues: T;\n validation?: FormValidation;\n onSubmit: (values: T) => void | Promise;\n onChange?: (values: T) => void;\n children: React.ReactNode;\n className?: string;\n testId?: string;\n}\n\nexport interface FieldProps {\n name: string;\n children: (field: FormField) => React.ReactNode;\n}\n\nexport interface FieldArrayProps {\n name: string;\n children: (fields: {\n items: T[];\n add: (item: T) => void;\n remove: (index: number) => void;\n move: (from: number, to: number) => void;\n replace: (index: number, item: T) => void;\n }) => React.ReactNode;\n}\n\n// Form hooks\nexport interface UseFormOptions = Record> {\n initialValues: T;\n validation?: FormValidation;\n onSubmit?: (values: T) => void | Promise;\n onChange?: (values: T) => void;\n validateOnChange?: boolean;\n validateOnBlur?: boolean;\n}\n\nexport interface UseFormReturn = Record> {\n values: T;\n errors: Partial>;\n touched: Partial>;\n dirty: boolean;\n valid: boolean;\n submitting: boolean;\n submitted: boolean;\n setValue: (name: K, value: T[K]) => void;\n setError: (name: K, error: string) => void;\n setTouched: (name: K, touched: boolean) => void;\n setFieldValue: (name: K, value: T[K]) => void;\n setFieldError: (name: K, error: string) => void;\n setFieldTouched: (name: K, touched: boolean) => void;\n validateField: (name: K) => void;\n validateForm: () => void;\n resetForm: (values?: T) => void;\n submitForm: () => void;\n handleSubmit: (event: React.FormEvent) => void;\n getFieldProps: (name: K) => FormField;\n}\n\n// Validation utilities\nexport interface ValidationError {\n field: string;\n message: string;\n code?: string;\n}\n\nexport interface ValidationResult {\n valid: boolean;\n errors: ValidationError[];\n}\n\n// Common validation rules\nexport const ValidationRules = {\n required:\n (message = \"This field is required\"): ValidationRule =>\n value =>\n !value || (typeof value === \"string\" && !value.trim()) ? message : undefined,\n\n email:\n (message = \"Please enter a valid email address\"): ValidationRule =>\n value => {\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return value && !emailRegex.test(value) ? message : undefined;\n },\n\n minLength:\n (min: number, message?: string): ValidationRule =>\n value => {\n const msg = message || `Must be at least ${min} characters`;\n return value && value.length < min ? msg : undefined;\n },\n\n maxLength:\n (max: number, message?: string): ValidationRule =>\n value => {\n const msg = message || `Must be no more than ${max} characters`;\n return value && value.length > max ? msg : undefined;\n },\n\n pattern:\n (regex: RegExp, message = \"Invalid format\"): ValidationRule =>\n value =>\n value && !regex.test(value) ? message : undefined,\n\n min:\n (min: number, message?: string): ValidationRule =>\n value => {\n const msg = message || `Must be at least ${min}`;\n return typeof value === \"number\" && value < min ? msg : undefined;\n },\n\n max:\n (max: number, message?: string): ValidationRule =>\n value => {\n const msg = message || `Must be no more than ${max}`;\n return typeof value === \"number\" && value > max ? msg : undefined;\n },\n\n phone:\n (message = \"Please enter a valid phone number\"): ValidationRule =>\n value => {\n const phoneRegex = /^\\+?[\\d\\s\\-\\(\\)]+$/;\n return value && !phoneRegex.test(value) ? message : undefined;\n },\n\n url:\n (message = \"Please enter a valid URL\"): ValidationRule =>\n value => {\n try {\n if (value) new URL(value);\n return undefined;\n } catch {\n return message;\n }\n },\n\n password:\n (\n message = \"Password must be at least 8 characters with uppercase, lowercase, and number\"\n ): ValidationRule =>\n value => {\n const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d@$!%*?&]{8,}$/;\n return value && !passwordRegex.test(value) ? message : undefined;\n },\n\n confirmPassword:\n (passwordField: string, message = \"Passwords do not match\"): ValidationRule =>\n (value: string) => {\n // Note: This would need access to form values in actual implementation\n // For now, just validate that value exists\n return !value ? message : undefined;\n },\n} as const;\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/types/subscription.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils/bundle-monitor.ts","messages":[{"ruleId":"@typescript-eslint/no-unsafe-assignment","severity":2,"message":"Unsafe assignment of an `any` value.","line":96,"column":17,"nodeType":"VariableDeclarator","messageId":"anyAssignment","endLine":96,"endColumn":42},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":96,"column":39,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":96,"endColumn":42,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[2562,2565],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[2562,2565],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .processingStart on an `any` value.","line":97,"column":26,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":97,"endColumn":41},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .startTime on an `any` value.","line":97,"column":56,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":97,"endColumn":65},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .processingStart on an `any` value.","line":98,"column":36,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":98,"endColumn":51},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .startTime on an `any` value.","line":98,"column":65,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":98,"endColumn":74},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":108,"column":26,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":108,"endColumn":29,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3030,3033],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3030,3033],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .hadRecentInput on an `any` value.","line":108,"column":31,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":108,"endColumn":45},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":109,"column":30,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":109,"endColumn":33,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[3082,3085],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[3082,3085],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .value on an `any` value.","line":109,"column":35,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":109,"endColumn":40}],"suppressedMessages":[],"errorCount":10,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"/**\n * Bundle size and performance monitoring utilities\n */\n\ninterface BundleMetrics {\n totalSize: number;\n gzippedSize: number;\n chunks: Array<{\n name: string;\n size: number;\n gzippedSize: number;\n }>;\n loadTime: number;\n timestamp: number;\n}\n\ninterface PerformanceMetrics {\n fcp: number; // First Contentful Paint\n lcp: number; // Largest Contentful Paint\n fid: number; // First Input Delay\n cls: number; // Cumulative Layout Shift\n ttfb: number; // Time to First Byte\n}\n\n/**\n * Monitor and report bundle metrics\n */\nexport class BundleMonitor {\n private static instance: BundleMonitor;\n private metrics: BundleMetrics[] = [];\n private performanceMetrics: PerformanceMetrics | null = null;\n\n static getInstance(): BundleMonitor {\n if (!BundleMonitor.instance) {\n BundleMonitor.instance = new BundleMonitor();\n }\n return BundleMonitor.instance;\n }\n\n /**\n * Record bundle metrics\n */\n recordBundleMetrics(metrics: Partial): void {\n const fullMetrics: BundleMetrics = {\n totalSize: 0,\n gzippedSize: 0,\n chunks: [],\n loadTime: 0,\n timestamp: Date.now(),\n ...metrics,\n };\n\n this.metrics.push(fullMetrics);\n\n // Keep only last 10 measurements\n if (this.metrics.length > 10) {\n this.metrics = this.metrics.slice(-10);\n }\n\n // Log to console in development\n if (process.env.NODE_ENV === \"development\") {\n console.log(\"Bundle Metrics:\", fullMetrics);\n }\n }\n\n /**\n * Measure and record Core Web Vitals\n */\n measureCoreWebVitals(): void {\n if (typeof window === \"undefined\") return;\n\n // Use the web-vitals library if available, otherwise use Performance API\n if (\"PerformanceObserver\" in window) {\n // Measure FCP\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n const fcp = entries.find(entry => entry.name === \"first-contentful-paint\");\n if (fcp) {\n this.updatePerformanceMetric(\"fcp\", fcp.startTime);\n }\n }).observe({ entryTypes: [\"paint\"] });\n\n // Measure LCP\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n const lastEntry = entries[entries.length - 1];\n if (lastEntry) {\n this.updatePerformanceMetric(\"lcp\", lastEntry.startTime);\n }\n }).observe({ entryTypes: [\"largest-contentful-paint\"] });\n\n // Measure FID\n new PerformanceObserver(list => {\n const entries = list.getEntries();\n entries.forEach(entry => {\n const eventEntry = entry as any; // Type assertion for processingStart\n if (eventEntry.processingStart && eventEntry.startTime) {\n const fid = eventEntry.processingStart - eventEntry.startTime;\n this.updatePerformanceMetric(\"fid\", fid);\n }\n });\n }).observe({ entryTypes: [\"first-input\"] });\n\n // Measure CLS\n new PerformanceObserver(list => {\n let cls = 0;\n list.getEntries().forEach(entry => {\n if (!(entry as any).hadRecentInput) {\n cls += (entry as any).value;\n }\n });\n this.updatePerformanceMetric(\"cls\", cls);\n }).observe({ entryTypes: [\"layout-shift\"] });\n\n // Measure TTFB\n const navigation = performance.getEntriesByType(\"navigation\")[0];\n if (navigation) {\n const ttfb = navigation.responseStart - navigation.requestStart;\n this.updatePerformanceMetric(\"ttfb\", ttfb);\n }\n }\n }\n\n private updatePerformanceMetric(metric: keyof PerformanceMetrics, value: number): void {\n if (!this.performanceMetrics) {\n this.performanceMetrics = {\n fcp: 0,\n lcp: 0,\n fid: 0,\n cls: 0,\n ttfb: 0,\n };\n }\n\n this.performanceMetrics[metric] = value;\n\n // Log to console in development\n if (process.env.NODE_ENV === \"development\") {\n console.log(`Core Web Vital - ${metric.toUpperCase()}:`, value);\n }\n\n // Report to analytics service if configured\n this.reportToAnalytics(metric, value);\n }\n\n /**\n * Get current performance metrics\n */\n getPerformanceMetrics(): PerformanceMetrics | null {\n return this.performanceMetrics;\n }\n\n /**\n * Get bundle metrics history\n */\n getBundleMetrics(): BundleMetrics[] {\n return [...this.metrics];\n }\n\n /**\n * Report metrics to analytics service\n */\n private reportToAnalytics(metric: string, value: number): void {\n // This would integrate with your analytics service\n // For now, we'll just store it locally\n if (typeof window !== \"undefined\" && window.localStorage) {\n const key = `perf_${metric}`;\n const data = {\n value,\n timestamp: Date.now(),\n url: window.location.pathname,\n };\n localStorage.setItem(key, JSON.stringify(data));\n }\n }\n\n /**\n * Check if performance is within acceptable thresholds\n */\n isPerformanceGood(): boolean {\n if (!this.performanceMetrics) return false;\n\n const thresholds = {\n fcp: 1800, // 1.8s\n lcp: 2500, // 2.5s\n fid: 100, // 100ms\n cls: 0.1, // 0.1\n ttfb: 800, // 800ms\n };\n\n return (\n this.performanceMetrics.fcp <= thresholds.fcp &&\n this.performanceMetrics.lcp <= thresholds.lcp &&\n this.performanceMetrics.fid <= thresholds.fid &&\n this.performanceMetrics.cls <= thresholds.cls &&\n this.performanceMetrics.ttfb <= thresholds.ttfb\n );\n }\n\n /**\n * Get performance score (0-100)\n */\n getPerformanceScore(): number {\n if (!this.performanceMetrics) return 0;\n\n const weights = {\n fcp: 0.15,\n lcp: 0.25,\n fid: 0.25,\n cls: 0.25,\n ttfb: 0.1,\n };\n\n const thresholds = {\n fcp: { good: 1800, poor: 3000 },\n lcp: { good: 2500, poor: 4000 },\n fid: { good: 100, poor: 300 },\n cls: { good: 0.1, poor: 0.25 },\n ttfb: { good: 800, poor: 1800 },\n };\n\n let totalScore = 0;\n\n Object.entries(this.performanceMetrics).forEach(([metric, value]) => {\n const threshold = thresholds[metric as keyof typeof thresholds];\n const weight = weights[metric as keyof typeof weights];\n\n let score = 100;\n if (value > threshold.poor) {\n score = 0;\n } else if (value > threshold.good) {\n score = 50;\n }\n\n totalScore += score * weight;\n });\n\n return Math.round(totalScore);\n }\n}\n\n// Export singleton instance\nexport const bundleMonitor = BundleMonitor.getInstance();\n\n// Auto-start monitoring in browser\nif (typeof window !== \"undefined\") {\n bundleMonitor.measureCoreWebVitals();\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils/css-variables.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils/dynamic-import.ts","messages":[{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":6,"column":61,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":6,"endColumn":64,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[188,191],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[188,191],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":13,"column":23,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":13,"endColumn":26,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[352,355],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[352,355],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-unsafe-member-access","severity":2,"message":"Unsafe member access .displayName on an `any` value.","line":13,"column":28,"nodeType":"Identifier","messageId":"unsafeMemberExpression","endLine":13,"endColumn":39},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":22,"column":59,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":22,"endColumn":62,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[561,564],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[561,564],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":33,"column":58,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":33,"endColumn":61,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[850,853],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[850,853],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]},{"ruleId":"@typescript-eslint/no-explicit-any","severity":2,"message":"Unexpected any. Specify a different type.","line":56,"column":72,"nodeType":"TSAnyKeyword","messageId":"unexpectedAny","endLine":56,"endColumn":75,"suggestions":[{"messageId":"suggestUnknown","fix":{"range":[1466,1469],"text":"unknown"},"desc":"Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."},{"messageId":"suggestNever","fix":{"range":[1466,1469],"text":"never"},"desc":"Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."}]}],"suppressedMessages":[],"errorCount":6,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { lazy, ComponentType } from \"react\";\n\n/**\n * Utility for creating lazy-loaded components with better error handling\n */\nexport function createLazyComponent>(\n importFn: () => Promise<{ default: T }>,\n displayName?: string\n): T {\n const LazyComponent = lazy(importFn);\n\n if (displayName) {\n (LazyComponent as any).displayName = `Lazy(${displayName})`;\n }\n\n return LazyComponent as unknown as T;\n}\n\n/**\n * Utility for creating lazy-loaded feature modules\n */\nexport function createLazyFeature>(\n featureName: string,\n componentName: string,\n importFn: () => Promise<{ default: T }>\n): T {\n return createLazyComponent(importFn, `${featureName}.${componentName}`);\n}\n\n/**\n * Preload a dynamic import for better UX\n */\nexport function preloadComponent(importFn: () => Promise): void {\n // Only preload in browser environment\n if (typeof window !== \"undefined\") {\n // Use requestIdleCallback if available, otherwise setTimeout\n if (\"requestIdleCallback\" in window) {\n window.requestIdleCallback(() => {\n importFn().catch(() => {\n // Silently ignore preload errors\n });\n });\n } else {\n setTimeout(() => {\n importFn().catch(() => {\n // Silently ignore preload errors\n });\n }, 100);\n }\n }\n}\n\n/**\n * Create a preloadable lazy component\n */\nexport function createPreloadableLazyComponent>(\n importFn: () => Promise<{ default: T }>,\n displayName?: string\n): T & { preload: () => void } {\n const LazyComponent = createLazyComponent(importFn, displayName) as T & { preload: () => void };\n\n LazyComponent.preload = () => preloadComponent(importFn);\n\n return LazyComponent;\n}\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils/route-preloader.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/lib/utils/sso.ts","messages":[{"ruleId":"prettier/prettier","severity":1,"message":"Delete `⏎`","line":13,"column":1,"nodeType":null,"messageId":"delete","endLine":14,"endColumn":1,"fix":{"range":[331,332],"text":""}}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":1,"source":"export function openSsoLink(url: string, options?: { newTab?: boolean }) {\n const { newTab = true } = options || {};\n try {\n if (newTab) {\n window.open(url, \"_blank\", \"noopener,noreferrer\");\n } else {\n window.location.href = url;\n }\n } catch {\n // Silent no-op; callers already handle errors/logging\n }\n}\n\n","usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/providers/query-provider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/shared/types/catalog.types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/types/world-countries.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"/home/tnarantuya/projects/new-portal-website/apps/portal/src/utils/currency.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}]