Assist_Design/eslint-report.json
T. Narantuya a95ec60859 Refactor address management and update related services for improved clarity and functionality
- Updated address retrieval in user service to replace billing info with a dedicated address method.
- Adjusted API endpoints to use `PATCH /api/me/address` for address updates instead of billing updates.
- Enhanced documentation to reflect changes in address management processes and API usage.
- Removed deprecated types and services related to billing address handling, streamlining the codebase.
2025-09-17 18:43:43 +09:00

1 line
695 KiB
JSON
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[{"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 <div className=\"animate-in fade-in duration-500\">\n <div className=\"flex items-center gap-3 mb-6\">\n {icon}\n <div>\n <h2 className=\"text-2xl font-bold text-gray-900\">{title}</h2>\n <p className=\"text-gray-600\">{description}</p>\n </div>\n </div>\n\n {/* Regular Plans */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-6 justify-items-center\">\n {regularPlans.map(plan => (\n <PlanCard key={plan.id} plan={plan} isFamily={false} />\n ))}\n </div>\n\n {/* Family Discount Plans */}\n {showFamilyDiscount && familyPlans.length > 0 && (\n <>\n <div className=\"flex items-center gap-2 mb-4\">\n <UsersIcon className=\"h-5 w-5 text-green-600\" />\n <h3 className=\"text-lg font-semibold text-green-900\">Family Discount Options</h3>\n <span className=\"bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full\">\n You qualify!\n </span>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 justify-items-center\">\n {familyPlans.map(plan => (\n <PlanCard key={plan.id} plan={plan} isFamily={true} />\n ))}\n </div>\n </>\n )}\n </div>\n );\n}\n\nfunction PlanCard({ plan, isFamily }: { plan: SimPlan; isFamily: boolean }) {\n return (\n <AnimatedCard variant={isFamily ? \"success\" : \"default\"} className=\"p-6 w-full max-w-sm\">\n <div className=\"flex items-start justify-between mb-3\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <DevicePhoneMobileIcon className=\"h-4 w-4 text-blue-600\" />\n <span className=\"font-bold text-sm text-gray-900\">{plan.dataSize}</span>\n </div>\n {isFamily && (\n <div className=\"flex items-center gap-1 mb-1\">\n <UsersIcon className=\"h-4 w-4 text-green-600\" />\n <span className=\"bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium\">\n Family\n </span>\n </div>\n )}\n </div>\n </div>\n\n <div className=\"mb-3\">\n <div className=\"flex items-baseline gap-1\">\n <CurrencyYenIcon className=\"h-4 w-4 text-gray-600\" />\n <span className=\"text-xl font-bold text-gray-900\">\n {plan.monthlyPrice?.toLocaleString()}\n </span>\n <span className=\"text-gray-600 text-sm\">/month</span>\n </div>\n {isFamily && (\n <div className=\"text-xs text-green-600 font-medium mt-1\">Discounted price</div>\n )}\n </div>\n\n <div className=\"mb-4\">\n <p className=\"text-xs text-gray-600 line-clamp-2\">{plan.description}</p>\n </div>\n\n <Button as=\"a\" href={`/catalog/sim/configure?plan=${plan.sku}`} className=\"w-full\" size=\"sm\">\n Configure\n </Button>\n </AnimatedCard>\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 <PageLayout\n title=\"SIM Plans\"\n description=\"Loading plans...\"\n icon={<DevicePhoneMobileIcon className=\"h-6 w-6\" />}\n >\n <div className=\"flex items-center justify-center py-12\">\n <LoadingSpinner size=\"lg\" label=\"Loading SIM plans...\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n const errorMessage = error instanceof Error ? error.message : \"An unexpected error occurred\";\n return (\n <PageLayout\n title=\"SIM Plans\"\n description=\"Error loading plans\"\n icon={<DevicePhoneMobileIcon className=\"h-6 w-6\" />}\n >\n <div className=\"rounded-lg bg-red-50 border border-red-200 p-6\">\n <div className=\"text-red-800 font-medium\">Failed to load SIM plans</div>\n <div className=\"text-red-600 text-sm mt-1\">{errorMessage}</div>\n <Button as=\"a\" href=\"/catalog\" className=\"flex items-center mt-4\">\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Services\n </Button>\n </div>\n </PageLayout>\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 <PageLayout\n title=\"SIM Plans\"\n description=\"Choose your mobile plan with flexible options\"\n icon={<DevicePhoneMobileIcon className=\"h-6 w-6\" />}\n >\n <div className=\"max-w-6xl mx-auto px-4\">\n {/* Navigation */}\n <div className=\"mb-6 flex justify-center\">\n <Button as=\"a\" href=\"/catalog\" variant=\"outline\" size=\"sm\" className=\"group\">\n <ArrowLeftIcon className=\"w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform duration-300\" />\n Back to Services\n </Button>\n </div>\n\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-gray-900 mb-4\">Choose Your SIM Plan</h1>\n <p className=\"text-xl text-gray-600 max-w-3xl mx-auto\">\n Wide range of data options and voice plans with both physical SIM and eSIM options.\n </p>\n </div>\n {/* Family Discount Banner */}\n {hasExistingSim && (\n <div className=\"mb-8 p-6 rounded-xl border-2 border-green-200 bg-gradient-to-r from-green-50 to-emerald-50\">\n <div className=\"flex items-start gap-4\">\n <div className=\"flex-shrink-0\">\n <UsersIcon className=\"h-8 w-8 text-green-600\" />\n </div>\n <div className=\"flex-1\">\n <h3 className=\"font-bold text-green-900 text-lg mb-2\">\n 🎉 Family Discount Available!\n </h3>\n <p className=\"text-green-800 mb-3\">\n You have existing SIM services, so you qualify for family discount pricing on\n additional lines.\n </p>\n <div className=\"flex flex-wrap gap-4 text-sm\">\n <div className=\"flex items-center gap-2\">\n <CheckIcon className=\"h-4 w-4 text-green-600\" />\n <span className=\"text-green-700\">Reduced monthly pricing</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <CheckIcon className=\"h-4 w-4 text-green-600\" />\n <span className=\"text-green-700\">Same great features</span>\n </div>\n <div className=\"flex items-center gap-2\">\n <CheckIcon className=\"h-4 w-4 text-green-600\" />\n <span className=\"text-green-700\">Easy to manage</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* Tab Navigation */}\n <div className=\"mb-8 flex justify-center\">\n <div className=\"border-b border-gray-200\">\n <nav className=\"-mb-px flex space-x-8\" aria-label=\"Tabs\">\n <button\n onClick={() => setActiveTab(\"data-voice\")}\n className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${\n activeTab === \"data-voice\"\n ? \"border-blue-500 text-blue-600\"\n : \"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300\"\n }`}\n >\n <PhoneIcon\n className={`h-5 w-5 transition-transform duration-300 ${activeTab === \"data-voice\" ? \"scale-110\" : \"\"}`}\n />\n Data + SMS/Voice\n {plansByType.DataSmsVoice.length > 0 && (\n <span\n className={`bg-blue-100 text-blue-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${\n activeTab === \"data-voice\" ? \"scale-110 bg-blue-200\" : \"\"\n }`}\n >\n {plansByType.DataSmsVoice.length}\n </span>\n )}\n </button>\n <button\n onClick={() => setActiveTab(\"data-only\")}\n className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${\n activeTab === \"data-only\"\n ? \"border-purple-500 text-purple-600\"\n : \"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300\"\n }`}\n >\n <GlobeAltIcon\n className={`h-5 w-5 transition-transform duration-300 ${activeTab === \"data-only\" ? \"scale-110\" : \"\"}`}\n />\n Data Only\n {plansByType.DataOnly.length > 0 && (\n <span\n className={`bg-purple-100 text-purple-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${\n activeTab === \"data-only\" ? \"scale-110 bg-purple-200\" : \"\"\n }`}\n >\n {plansByType.DataOnly.length}\n </span>\n )}\n </button>\n <button\n onClick={() => setActiveTab(\"voice-only\")}\n className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 transition-all duration-300 ease-in-out transform hover:scale-105 ${\n activeTab === \"voice-only\"\n ? \"border-orange-500 text-orange-600\"\n : \"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300\"\n }`}\n >\n <PhoneIcon\n className={`h-5 w-5 transition-transform duration-300 ${activeTab === \"voice-only\" ? \"scale-110\" : \"\"}`}\n />\n Voice Only\n {plansByType.VoiceOnly.length > 0 && (\n <span\n className={`bg-orange-100 text-orange-600 text-xs px-2 py-0.5 rounded-full transition-all duration-300 ${\n activeTab === \"voice-only\" ? \"scale-110 bg-orange-200\" : \"\"\n }`}\n >\n {plansByType.VoiceOnly.length}\n </span>\n )}\n </button>\n </nav>\n </div>\n </div>\n\n {/* Tab Content */}\n <div className=\"min-h-[400px] relative\">\n <div\n className={`transition-all duration-500 ease-in-out ${\n activeTab === \"data-voice\"\n ? \"opacity-100 translate-y-0\"\n : \"opacity-0 translate-y-4 absolute inset-0 pointer-events-none\"\n }`}\n >\n {activeTab === \"data-voice\" && (\n <PlanTypeSection\n title=\"Data + SMS/Voice Plans\"\n description=\"Internet, calling, and SMS included\"\n icon={<PhoneIcon className=\"h-8 w-8 text-blue-600\" />}\n plans={plansByType.DataSmsVoice}\n showFamilyDiscount={hasExistingSim}\n />\n )}\n </div>\n\n <div\n className={`transition-all duration-500 ease-in-out ${\n activeTab === \"data-only\"\n ? \"opacity-100 translate-y-0\"\n : \"opacity-0 translate-y-4 absolute inset-0 pointer-events-none\"\n }`}\n >\n {activeTab === \"data-only\" && (\n <PlanTypeSection\n title=\"Data Only Plans\"\n description=\"Internet access for tablets, laptops, and IoT devices\"\n icon={<GlobeAltIcon className=\"h-8 w-8 text-purple-600\" />}\n plans={plansByType.DataOnly}\n showFamilyDiscount={hasExistingSim}\n />\n )}\n </div>\n\n <div\n className={`transition-all duration-500 ease-in-out ${\n activeTab === \"voice-only\"\n ? \"opacity-100 translate-y-0\"\n : \"opacity-0 translate-y-4 absolute inset-0 pointer-events-none\"\n }`}\n >\n {activeTab === \"voice-only\" && (\n <PlanTypeSection\n title=\"Voice Only Plans\"\n description=\"Traditional calling and SMS without internet\"\n icon={<PhoneIcon className=\"h-8 w-8 text-orange-600\" />}\n plans={plansByType.VoiceOnly}\n showFamilyDiscount={hasExistingSim}\n />\n )}\n </div>\n </div>\n\n {/* Features Section */}\n <div className=\"mt-8 bg-gray-50 rounded-2xl p-8 max-w-4xl mx-auto\">\n <h3 className=\"font-bold text-gray-900 text-xl mb-6 text-center\">\n Plan Features & Terms\n </h3>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 text-sm\">\n <div className=\"flex items-start gap-3\">\n <CheckIcon className=\"h-5 w-5 text-green-500 mt-0.5 flex-shrink-0\" />\n <div>\n <div className=\"font-medium text-gray-900\">3-Month Contract</div>\n <div className=\"text-gray-600\">Minimum 3 billing months</div>\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <CheckIcon className=\"h-5 w-5 text-green-500 mt-0.5 flex-shrink-0\" />\n <div>\n <div className=\"font-medium text-gray-900\">First Month Free</div>\n <div className=\"text-gray-600\">Basic fee waived initially</div>\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <CheckIcon className=\"h-5 w-5 text-green-500 mt-0.5 flex-shrink-0\" />\n <div>\n <div className=\"font-medium text-gray-900\">5G Network</div>\n <div className=\"text-gray-600\">High-speed coverage</div>\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <CheckIcon className=\"h-5 w-5 text-green-500 mt-0.5 flex-shrink-0\" />\n <div>\n <div className=\"font-medium text-gray-900\">eSIM Support</div>\n <div className=\"text-gray-600\">Digital activation</div>\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <CheckIcon className=\"h-5 w-5 text-green-500 mt-0.5 flex-shrink-0\" />\n <div>\n <div className=\"font-medium text-gray-900\">Family Discounts</div>\n <div className=\"text-gray-600\">Multi-line savings</div>\n </div>\n </div>\n <div className=\"flex items-start gap-3\">\n <CheckIcon className=\"h-5 w-5 text-green-500 mt-0.5 flex-shrink-0\" />\n <div>\n <div className=\"font-medium text-gray-900\">Plan Switching</div>\n <div className=\"text-gray-600\">Free data plan changes</div>\n </div>\n </div>\n </div>\n </div>\n\n {/* Info Section */}\n <div className=\"mt-8 p-6 rounded-lg border border-blue-200 bg-blue-50 max-w-4xl mx-auto\">\n <div className=\"flex items-start gap-3 mb-4\">\n <InformationCircleIcon className=\"h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0\" />\n <div className=\"text-sm\">\n <div className=\"font-medium text-blue-900 mb-2\">Important Terms & Conditions</div>\n </div>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm\">\n <div className=\"space-y-3\">\n <div>\n <div className=\"font-medium text-blue-900\">Contract Period</div>\n <p className=\"text-blue-800\">\n Minimum 3 full billing months required. First month (sign-up to end of month) is\n free and doesn&apos;t count toward contract.\n </p>\n </div>\n <div>\n <div className=\"font-medium text-blue-900\">Billing Cycle</div>\n <p className=\"text-blue-800\">\n Monthly billing from 1st to end of month. Regular billing starts on 1st of\n following month after sign-up.\n </p>\n </div>\n <div>\n <div className=\"font-medium text-blue-900\">Cancellation</div>\n <p className=\"text-blue-800\">\n Can be requested online after 3rd month. Service terminates at end of billing\n cycle.\n </p>\n </div>\n </div>\n <div className=\"space-y-3\">\n <div>\n <div className=\"font-medium text-blue-900\">Plan Changes</div>\n <p className=\"text-blue-800\">\n Data plan switching is free and takes effect next month. Voice plan changes\n require new SIM and cancellation policies apply.\n </p>\n </div>\n <div>\n <div className=\"font-medium text-blue-900\">Calling/SMS Charges</div>\n <p className=\"text-blue-800\">\n Pay-per-use charges apply separately. Billed 5-6 weeks after usage within billing\n cycle.\n </p>\n </div>\n <div>\n <div className=\"font-medium text-blue-900\">SIM Replacement</div>\n <p className=\"text-blue-800\">\n Reissue fee of 1,500 JPY applies for damaged, lost, or replacement SIM cards.\n </p>\n </div>\n </div>\n </div>\n </div>\n </div>\n </PageLayout>\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<string | null>(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 <div className=\"flex items-center justify-center h-64\">\n <div className=\"text-center space-y-4\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"text-muted-foreground\">Loading dashboard...</p>\n </div>\n </div>\n );\n }\n\n // Handle error state\n if (error) {\n return (\n <ErrorState\n title=\"Error loading dashboard\"\n message={error instanceof Error ? error.message : \"An unexpected error occurred\"}\n variant=\"page\"\n />\n );\n }\n\n return (\n <>\n <div className=\"min-h-screen bg-gradient-to-br from-slate-50 to-blue-50\">\n <div className=\"max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8 py-[var(--cp-space-2xl)]\">\n {/* Modern Header */}\n <div className=\"mb-[var(--cp-space-3xl)]\">\n <div className=\"flex items-start justify-between gap-[var(--cp-space-xl)]\">\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-3 flex-wrap\">\n <h1 className=\"text-3xl font-bold leading-tight text-gray-900\">\n Welcome back, {user?.firstName || user?.email?.split(\"@\")[0] || \"User\"}!\n </h1>\n {/* Tasks chip */}\n <TasksChip summaryLoading={summaryLoading} summary={summary} />\n </div>\n <div className=\"mt-1 text-sm text-gray-500\">Portal / Dashboard</div>\n </div>\n {/* No duplicate page-level CTAs here per guidelines */}\n </div>\n </div>\n\n {/* Modern Stats Grid */}\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--cp-space-2xl)] mb-[var(--cp-space-3xl)]\">\n <StatCard\n title=\"Recent Orders\"\n value={summary?.stats?.recentOrders || 0}\n icon={ClipboardDocumentListIconSolid}\n gradient=\"from-gray-500 to-gray-600\"\n href=\"/orders\"\n loading={summaryLoading}\n error={!!error}\n />\n <StatCard\n title=\"Pending Invoices\"\n value={summary?.stats?.unpaidInvoices || 0}\n icon={CreditCardIconSolid}\n gradient={\n (summary?.stats?.unpaidInvoices ?? 0) > 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 <StatCard\n title=\"Active Services\"\n value={summary?.stats?.activeSubscriptions || 0}\n icon={ServerIconSolid}\n gradient=\"from-blue-500 to-cyan-500\"\n href=\"/subscriptions\"\n loading={summaryLoading}\n error={!!error}\n />\n <StatCard\n title=\"Support Cases\"\n value={summary?.stats?.openCases || 0}\n icon={ChatBubbleLeftRightIconSolid}\n gradient={\n (summary?.stats?.openCases ?? 0) > 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 </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-[var(--cp-space-3xl)]\">\n {/* Main Content Area */}\n <div className=\"lg:col-span-2 space-y-[var(--cp-space-3xl)]\">\n {/* Upcoming Payment - compressed attention banner */}\n {summary?.nextInvoice && (\n <div\n id=\"attention\"\n className=\"bg-white rounded-xl border border-orange-200 shadow-sm p-4\"\n >\n <div className=\"flex items-center gap-4\">\n <div className=\"flex-shrink-0\">\n <div className=\"w-10 h-10 rounded-md bg-gradient-to-r from-amber-500 to-orange-500 flex items-center justify-center\">\n <CalendarDaysIcon className=\"h-5 w-5 text-white\" />\n </div>\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2 text-sm text-gray-700\">\n <span className=\"font-semibold text-gray-900\">Upcoming Payment</span>\n <span className=\"text-gray-400\">•</span>\n <span>Invoice #{summary.nextInvoice.id}</span>\n <span className=\"text-gray-400\">•</span>\n <span title={format(new Date(summary.nextInvoice.dueDate), \"MMMM d, yyyy\")}>\n Due{\" \"}\n {formatDistanceToNow(new Date(summary.nextInvoice.dueDate), {\n addSuffix: true,\n })}\n </span>\n </div>\n <div className=\"mt-1 text-2xl font-bold text-gray-900\">\n {formatCurrency(summary.nextInvoice.amount, {\n currency: summary.nextInvoice.currency || \"JPY\",\n locale: getCurrencyLocale(summary.nextInvoice.currency || \"JPY\"),\n })}\n </div>\n <div className=\"mt-1 text-xs text-gray-500\">\n Exact due date:{\" \"}\n {format(new Date(summary.nextInvoice.dueDate), \"MMMM d, yyyy\")}\n </div>\n </div>\n <div className=\"flex flex-col items-end gap-2\">\n <button\n onClick={() => 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 <span className=\"mr-2\">\n <span className=\"sr-only\">Opening...</span>\n </span>\n ) : null}\n {paymentLoading ? \"Opening Payment...\" : \"Pay Now\"}\n {!paymentLoading && <ChevronRightIcon className=\"ml-2 h-4 w-4\" />}\n </button>\n <Link\n href={`/billing/invoices/${summary.nextInvoice.id}`}\n className=\"text-blue-600 hover:text-blue-700 font-medium text-sm\"\n >\n View invoice\n </Link>\n </div>\n </div>\n </div>\n )}\n\n {/* Payment Error Display */}\n {paymentError && (\n <ErrorState\n title=\"Payment Error\"\n message={paymentError}\n variant=\"inline\"\n onRetry={() => setPaymentError(null)}\n retryLabel=\"Dismiss\"\n />\n )}\n\n {/* Recent Activity - filtered list */}\n <ActivityFeed\n activities={summary?.recentActivity || []}\n onItemClick={handleActivityClick}\n loading={summaryLoading}\n error={error ? String(error) : null}\n maxItems={10}\n showFilter={true}\n />\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-[var(--cp-space-2xl)]\">\n {/* Quick Actions - simplified */}\n <div className=\"bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden\">\n <div className=\"px-6 py-4 border-b border-gray-100\">\n <h3 className=\"text-lg font-semibold text-gray-900\">Quick Actions</h3>\n </div>\n <div className=\"p-[var(--cp-space-2xl)] space-y-[var(--cp-space-lg)]\">\n <QuickAction\n href=\"/billing/invoices\"\n title=\"View invoices\"\n description=\"Review and pay invoices\"\n icon={DocumentTextIcon}\n iconColor=\"text-blue-600\"\n bgColor=\"bg-blue-50\"\n />\n <QuickAction\n href=\"/subscriptions\"\n title=\"Manage services\"\n description=\"View active subscriptions\"\n icon={ServerIcon}\n iconColor=\"text-blue-600\"\n bgColor=\"bg-blue-50\"\n />\n <QuickAction\n href=\"/support/new\"\n title=\"Get support\"\n description=\"Open a support ticket\"\n icon={ChatBubbleLeftRightIcon}\n iconColor=\"text-blue-600\"\n bgColor=\"bg-blue-50\"\n />\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\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 <button\n onClick={() => {\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 </button>\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<PlanCode, string> = {\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<string>(\"\");\n const [newPlanCode, setNewPlanCode] = useState<\"\" | PlanCode>(\"\");\n const [assignGlobalIp, setAssignGlobalIp] = useState(false);\n const [scheduledAt, setScheduledAt] = useState(\"\");\n const [message, setMessage] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(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 <div className=\"max-w-3xl mx-auto p-6\">\n <div className=\"mb-4\">\n <Link\n href={`/subscriptions/${subscriptionId}#sim-management`}\n className=\"text-blue-600 hover:text-blue-700\"\n >\n ← Back to SIM Management\n </Link>\n </div>\n <div className=\"bg-white rounded-xl border border-gray-200 p-6\">\n <h1 className=\"text-xl font-semibold text-gray-900 mb-1\">Change Plan</h1>\n <p className=\"text-sm text-gray-600 mb-6\">\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 </p>\n {message && (\n <div className=\"mb-4 text-green-700 bg-green-50 border border-green-200 rounded p-3\">\n {message}\n </div>\n )}\n {error && (\n <div className=\"mb-4 text-red-700 bg-red-50 border border-red-200 rounded p-3\">\n {error}\n </div>\n )}\n\n <form onSubmit={submit} className=\"space-y-6\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">New Plan</label>\n <select\n value={newPlanCode}\n onChange={e => setNewPlanCode(e.target.value as PlanCode)}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-md\"\n >\n <option value=\"\">Choose a plan</option>\n {options.map(code => (\n <option key={code} value={code}>\n {PLAN_LABELS[code]}\n </option>\n ))}\n </select>\n </div>\n\n <div className=\"flex items-center\">\n <input\n id=\"globalip\"\n type=\"checkbox\"\n checked={assignGlobalIp}\n onChange={e => setAssignGlobalIp(e.target.checked)}\n className=\"h-4 w-4 text-blue-600 border-gray-300 rounded\"\n />\n <label htmlFor=\"globalip\" className=\"ml-2 text-sm text-gray-700\">\n Assign global IP\n </label>\n </div>\n\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">\n Schedule (optional)\n </label>\n <input\n type=\"date\"\n value={scheduledAt}\n onChange={e => setScheduledAt(e.target.value)}\n className=\"w-full px-3 py-2 border border-gray-300 rounded-md\"\n />\n </div>\n\n <div className=\"flex gap-3\">\n <button\n type=\"submit\"\n disabled={loading}\n className=\"px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50\"\n >\n {loading ? \"Processing…\" : \"Submit Plan Change\"}\n </button>\n <Link\n href={`/subscriptions/${subscriptionId}#sim-management`}\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 </Link>\n </div>\n </form>\n </div>\n </div>\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 <div className={`pb-4 border-b border-gray-200 ${className || \"\"}`}>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center\">\n {leftIcon}\n <div className={leftIcon ? \"ml-3\" : undefined}>\n <h3 className=\"text-lg font-medium text-gray-900\">{title}</h3>\n {subtitle && <p className=\"text-sm text-gray-500\">{subtitle}</p>}\n </div>\n </div>\n {status && <StatusPill label={status.label} variant={status.variant} />}\n {actions}\n </div>\n {meta && <div className=\"mt-4\">{meta}</div>}\n </div>\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 <div className={`px-1 sm:px-0 py-1 flex items-center justify-between ${className || \"\"}`}>\n <div className=\"flex-1 flex justify-between sm:hidden\">\n <button\n onClick={() => 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 </button>\n <button\n onClick={() => 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 </button>\n </div>\n <div className=\"hidden sm:flex-1 sm:flex sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm text-gray-700\">\n Showing <span className=\"font-medium\">{(currentPage - 1) * pageSize + 1}</span> to {\" \"}\n <span className=\"font-medium\">{Math.min(currentPage * pageSize, totalItems)}</span> of {\" \"}\n <span className=\"font-medium\">{totalItems}</span> results\n </p>\n </div>\n <div>\n <nav className=\"relative z-0 inline-flex rounded-md shadow-sm -space-x-px\" aria-label=\"Pagination\">\n <button\n onClick={() => onPageChange(Math.max(1, currentPage - 1))}\n disabled={!canPrev}\n className=\"relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Previous\n </button>\n <button\n onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}\n disabled={!canNext}\n className=\"relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Next\n </button>\n </nav>\n </div>\n </div>\n </div>\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<React.SVGProps<SVGSVGElement>>;\n tooltip?: string;\n}\n\ninterface NavigationItem {\n name: string;\n href?: string;\n icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;\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<string[]>(() => {\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 <div className=\"min-h-screen bg-background flex items-center justify-center\">\n <div className=\"text-center space-y-3\">\n <LoadingSpinner size=\"xl\" variant=\"current\" />\n <p className=\"text-muted-foreground\">Loading...</p>\n </div>\n </div>\n );\n }\n\n return (\n <>\n <div className=\"h-screen flex overflow-hidden bg-background\">\n {/* Mobile sidebar overlay */}\n {sidebarOpen && (\n <div className=\"fixed inset-0 flex z-50 md:hidden\">\n <div\n className=\"fixed inset-0 bg-black/50 animate-in fade-in duration-300\"\n onClick={() => setSidebarOpen(false)}\n />\n <div className=\"relative flex-1 flex flex-col max-w-xs w-full bg-[var(--cp-sidebar-bg)] border-r border-[var(--cp-sidebar-border)] animate-in slide-in-from-left duration-300 shadow-2xl\">\n <div className=\"absolute top-0 right-0 -mr-12 pt-2\">\n <button\n type=\"button\"\n className=\"ml-1 flex items-center justify-center h-10 w-10 rounded-full bg-white/10 backdrop-blur-sm text-white hover:bg-white/20 focus:outline-none focus:ring-2 focus:ring-white/50 transition-colors duration-200\"\n onClick={() => setSidebarOpen(false)}\n >\n <XMarkIcon className=\"h-6 w-6\" />\n </button>\n </div>\n <Sidebar\n navigation={navigation}\n pathname={pathname}\n expandedItems={expandedItems}\n toggleExpanded={toggleExpanded}\n isMobile\n />\n </div>\n </div>\n )}\n\n {/* Desktop sidebar */}\n <div className=\"hidden md:flex md:flex-shrink-0\">\n <div className=\"flex flex-col w-[240px] border-r border-[var(--cp-sidebar-border)] bg-[var(--cp-sidebar-bg)] shadow-sm\">\n <Sidebar\n navigation={navigation}\n pathname={pathname}\n expandedItems={expandedItems}\n toggleExpanded={toggleExpanded}\n />\n </div>\n </div>\n\n {/* Main content */}\n <div className=\"flex flex-col w-0 flex-1 overflow-hidden\">\n {/* Header */}\n <Header onMenuClick={() => setSidebarOpen(true)} user={user} />\n\n {/* Main content area */}\n <main className=\"flex-1 relative overflow-y-auto focus:outline-none\">{children}</main>\n </div>\n </div>\n\n {/* Session timeout warning */}\n <SessionTimeoutWarning />\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 <div className=\"flex flex-col h-0 flex-1 bg-[var(--cp-sidebar-bg)]\">\n {/* Logo Section */}\n <div className=\"flex items-center flex-shrink-0 h-16 px-6 border-b border-[var(--cp-sidebar-border)]\">\n <div className=\"flex items-center space-x-3\">\n <div className=\"p-2 bg-white rounded-xl border border-[var(--cp-sidebar-border)] shadow-sm\">\n <Logo size={20} />\n </div>\n <div>\n <span className=\"text-base font-bold text-[var(--cp-sidebar-text)]\">\n Assist Solutions\n </span>\n <p className=\"text-xs text-[var(--cp-sidebar-text)]/60\">Customer Portal</p>\n </div>\n </div>\n </div>\n\n {/* Navigation */}\n <div className=\"flex-1 flex flex-col pt-6 pb-4 overflow-y-auto\">\n <nav className=\"flex-1 px-3 space-y-1\">\n {navigation.map(item => (\n <NavigationItem\n key={item.name}\n item={item}\n pathname={pathname}\n isExpanded={expandedItems.includes(item.name)}\n toggleExpanded={toggleExpanded}\n />\n ))}\n </nav>\n </div>\n </div>\n );\n});\n\ninterface HeaderProps {\n onMenuClick: () => void;\n user: any;\n}\n\nconst Header = memo(function Header({ onMenuClick, user }: HeaderProps) {\n return (\n <div className=\"bg-[var(--cp-header-bg)] border-b border-[var(--cp-header-border)] backdrop-blur-sm\">\n <div className=\"flex items-center h-16 gap-3 px-4 sm:px-6\">\n {/* Mobile menu button */}\n <button\n type=\"button\"\n className=\"md:hidden p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20\"\n onClick={onMenuClick}\n aria-label=\"Open navigation\"\n >\n <Bars3Icon className=\"h-6 w-6\" />\n </button>\n\n {/* Spacer */}\n <div className=\"flex-1\" />\n\n {/* Global Utilities */}\n <div className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n className=\"relative p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20\"\n aria-label=\"Notifications\"\n >\n <BellIcon className=\"h-5 w-5\" />\n <span className=\"absolute top-1 right-1 h-2 w-2 bg-red-500 rounded-full\"></span>\n </button>\n\n <Link\n href=\"/support/kb\"\n aria-label=\"Help\"\n className=\"hidden sm:inline-flex p-2 rounded-lg text-gray-600 hover:text-gray-900 hover:bg-gray-100 transition-colors\"\n title=\"Help Center\"\n >\n <QuestionMarkCircleIcon className=\"h-5 w-5\" />\n </Link>\n\n <Link\n href=\"/account/profile\"\n className=\"hidden sm:inline-flex items-center px-2.5 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200\"\n >\n {user?.firstName || user?.email?.split(\"@\")[0] || \"Account\"}\n </Link>\n </div>\n </div>\n </div>\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 <div className=\"relative\">\n <button\n onClick={() => 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 <div className=\"absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary rounded-r-full\" />\n )}\n\n <div\n className={`p-1.5 rounded-md mr-3 transition-colors duration-200 ${\n isActive\n ? \"bg-primary/10 text-primary\"\n : \"text-[var(--cp-sidebar-text)]/70 group-hover:text-[var(--cp-sidebar-text-hover)] group-hover:bg-gray-100\"\n }`}\n >\n <item.icon className=\"h-5 w-5\" />\n </div>\n\n <span className=\"flex-1 font-medium\">{item.name}</span>\n\n <svg\n className={`h-4 w-4 transition-transform duration-200 ease-out ${\n isExpanded ? \"rotate-90\" : \"\"\n } ${isActive ? \"text-primary\" : \"text-[var(--cp-sidebar-text)]/50\"}`}\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </button>\n\n {/* Animated dropdown */}\n <div\n className={`overflow-hidden transition-all duration-300 ease-out ${\n isExpanded ? \"max-h-96 opacity-100\" : \"max-h-0 opacity-0\"\n }`}\n >\n <div className=\"mt-1 ml-6 space-y-0.5 border-l border-[var(--cp-sidebar-border)] pl-4\">\n {item.children?.map((child: NavigationChild) => {\n const isChildActive = pathname === (child.href || \"\").split(/[?#]/)[0];\n return (\n <Link\n key={child.name}\n href={child.href}\n className={`group flex items-center px-3 py-2 text-sm rounded-md transition-all duration-200 relative ${\n isChildActive\n ? \"text-[var(--cp-sidebar-active-text)] bg-[var(--cp-sidebar-active-bg)] font-medium\"\n : \"text-[var(--cp-sidebar-text)]/80 hover:text-[var(--cp-sidebar-text-hover)] hover:bg-[var(--cp-sidebar-hover-bg)]\"\n }`}\n title={child.tooltip || child.name}\n aria-current={isChildActive ? \"page\" : undefined}\n >\n {/* Child active indicator */}\n {isChildActive && (\n <div className=\"absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-primary rounded-full\" />\n )}\n\n <div\n className={`w-1.5 h-1.5 rounded-full mr-3 transition-colors duration-200 ${\n isChildActive\n ? \"bg-primary\"\n : \"bg-[var(--cp-sidebar-text)]/30 group-hover:bg-[var(--cp-sidebar-text)]/50\"\n }`}\n />\n\n <span className=\"truncate\">{child.name}</span>\n </Link>\n );\n })}\n </div>\n </div>\n </div>\n );\n }\n\n if (item.isLogout) {\n return (\n <button\n onClick={handleLogout}\n className=\"group w-full flex items-center px-3 py-2.5 text-sm font-medium text-red-600 hover:text-red-700 hover:bg-red-50 rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-red-200\"\n >\n <div className=\"p-1.5 rounded-md mr-3 text-red-500 group-hover:text-red-600 group-hover:bg-red-100 transition-colors duration-200\">\n <item.icon className=\"h-5 w-5\" />\n </div>\n <span>{item.name}</span>\n </button>\n );\n }\n\n return (\n <Link\n href={item.href || \"#\"}\n className={`group w-full flex items-center px-3 py-2.5 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 aria-current={isActive ? \"page\" : undefined}\n >\n {/* Active indicator */}\n {isActive && (\n <div className=\"absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-primary rounded-r-full\" />\n )}\n\n <div\n className={`p-1.5 rounded-md mr-3 transition-colors duration-200 ${\n isActive\n ? \"bg-primary/10 text-primary\"\n : \"text-[var(--cp-sidebar-text)]/70 group-hover:text-[var(--cp-sidebar-text-hover)] group-hover:bg-gray-100\"\n }`}\n >\n <item.icon className=\"h-5 w-5\" />\n </div>\n\n <span className=\"truncate\">{item.name}</span>\n </Link>\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<typeof buttonVariants> {\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<HTMLAnchorElement> &\n BaseButtonProps;\n\ntype ButtonAsButtonProps = {\n as?: \"button\";\n} & ButtonHTMLAttributes<HTMLButtonElement> &\n BaseButtonProps;\n\nexport type ButtonProps = ButtonAsAnchorProps | ButtonAsButtonProps;\n\nconst Button = forwardRef<HTMLButtonElement | HTMLAnchorElement, ButtonProps>((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 <ArrowPathIcon className=\"mr-2 h-4 w-4 animate-spin\" />\n ) : (\n leftIcon && <span className=\"mr-2\">{leftIcon}</span>\n )}\n {loading && loadingText ? loadingText : children}\n {!loading && rightIcon && <span className=\"ml-2\">{rightIcon}</span>}\n </>\n );\n\n if (props.as === \"a\") {\n const { as: _as, href, ...anchorProps } = restProps as ButtonAsAnchorProps;\n return (\n <a\n className={cn(buttonVariants({ variant, size, className }))}\n href={href}\n ref={ref as React.Ref<HTMLAnchorElement>}\n {...anchorProps}\n >\n {content}\n </a>\n );\n }\n\n const { as: _as, ...buttonProps } = restProps as ButtonAsButtonProps;\n return (\n <button\n className={cn(buttonVariants({ variant, size, className }))}\n disabled={isDisabled}\n ref={ref as React.Ref<HTMLButtonElement>}\n {...buttonProps}\n >\n {content}\n </button>\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 <div\n className={cn(\n \"bg-[var(--cp-skeleton-base)] rounded-md\",\n animate && \"animate-pulse\",\n className\n )}\n />\n );\n}\n\nexport function LoadingCard({ className }: { className?: string }) {\n return (\n <div\n className={cn(\n \"bg-white border border-gray-200 rounded-[var(--cp-card-radius)] p-[var(--cp-card-padding)] shadow-[var(--cp-card-shadow)]\",\n className\n )}\n >\n <div className=\"space-y-4\">\n <div className=\"flex items-center space-x-3\">\n <Skeleton className=\"h-8 w-8 rounded-full\" />\n <div className=\"space-y-2 flex-1\">\n <Skeleton className=\"h-4 w-1/3\" />\n <Skeleton className=\"h-3 w-1/2\" />\n </div>\n </div>\n <div className=\"space-y-2\">\n <Skeleton className=\"h-3 w-full\" />\n <Skeleton className=\"h-3 w-4/5\" />\n <Skeleton className=\"h-3 w-3/5\" />\n </div>\n </div>\n </div>\n );\n}\n\nexport function LoadingTable({ rows = 5, columns = 4 }: { rows?: number; columns?: number }) {\n return (\n <div className=\"bg-white border border-gray-200 rounded-[var(--cp-card-radius)] overflow-hidden\">\n {/* Header */}\n <div className=\"border-b border-gray-200 p-4\">\n <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }).map((_, i) => (\n <Skeleton key={i} className=\"h-4 w-20\" />\n ))}\n </div>\n </div>\n\n {/* Rows */}\n <div className=\"divide-y divide-gray-200\">\n {Array.from({ length: rows }).map((_, rowIndex) => (\n <div key={rowIndex} className=\"p-4\">\n <div className=\"grid gap-4\" style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>\n {Array.from({ length: columns }).map((_, colIndex) => (\n <Skeleton key={colIndex} className=\"h-4 w-full\" />\n ))}\n </div>\n </div>\n ))}\n </div>\n </div>\n );\n}\n\nexport function LoadingStats({ count = 4 }: { count?: number }) {\n return (\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6\">\n {Array.from({ length: count }).map((_, i) => (\n <div\n key={i}\n className=\"bg-white border border-gray-200 rounded-[var(--cp-card-radius)] p-[var(--cp-card-padding)] shadow-[var(--cp-card-shadow)]\"\n >\n <div className=\"flex items-center\">\n <Skeleton className=\"h-8 w-8 rounded-full\" />\n <div className=\"ml-4 space-y-2 flex-1\">\n <Skeleton className=\"h-4 w-16\" />\n <Skeleton className=\"h-6 w-12\" />\n </div>\n </div>\n </div>\n ))}\n </div>\n );\n}\n\nexport function PageLoadingState({ title }: { title: string }) {\n return (\n <div className=\"py-8\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 md:px-8\">\n {/* Header skeleton */}\n <div className=\"mb-8\">\n <div className=\"flex items-center\">\n <Skeleton className=\"h-8 w-8 mr-3\" />\n <div className=\"space-y-2\">\n <Skeleton className=\"h-8 w-48\" />\n <Skeleton className=\"h-4 w-64\" />\n </div>\n </div>\n </div>\n\n {/* Content skeleton */}\n <div className=\"space-y-6\">\n <LoadingStats />\n <LoadingTable />\n </div>\n </div>\n </div>\n );\n}\n\nexport function FullPageLoadingState({ title }: { title: string }) {\n return (\n <div className=\"min-h-screen bg-background flex items-center justify-center\">\n <div className=\"text-center space-y-4\">\n <div className=\"animate-spin rounded-full h-16 w-16 border-4 border-gray-200 border-t-primary mx-auto\"></div>\n <div className=\"space-y-2\">\n <h2 className=\"text-xl font-semibold text-foreground\">{title}</h2>\n <p className=\"text-muted-foreground\">Please wait while we load your content...</p>\n </div>\n </div>\n </div>\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<AddressData>`.","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<string | null>(null);\n const [pwdSuccess, setPwdSuccess] = useState<string | null>(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 <PageLayout\n title={user?.firstName ? `${user.firstName} ${user.lastName || \"\"}` : \"Profile\"}\n description=\"Manage your personal information and address\"\n icon={<></>}\n >\n <div className=\"space-y-8\">\n <PersonalInfoCard\n data={formData}\n isEditing={isEditingInfo}\n isSaving={isSavingProfile}\n onEdit={() => 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 <AddressCard\n address={addressData as any}\n isEditing={isEditingAddress}\n isSaving={isSavingAddress}\n error={error}\n onEdit={() => 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 <PasswordChangeCard\n isChanging={isChangingPassword}\n error={pwdError}\n success={pwdSuccess}\n form={pwdForm}\n setForm={next => setPwdForm(prev => ({ ...prev, ...next }))}\n onSubmit={() => {\n void handleChangePassword();\n }}\n />\n </div>\n </PageLayout>\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<string | null>(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 <div className=\"py-6\">\n <div className=\"max-w-4xl mx-auto px-4 sm:px-6 md:px-8\">\n <div className=\"flex items-center justify-center py-12\">\n <LoadingSpinner size=\"lg\" />\n <span className=\"ml-3 text-gray-600\">Loading profile...</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"py-6\">\n <div className=\"max-w-4xl mx-auto px-4 sm:px-6 md:px-8\">\n {error && (\n <div className=\"mb-6 bg-red-50 border border-red-200 rounded-xl p-4\">\n <div className=\"flex items-start space-x-3\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-red-500 mt-0.5 flex-shrink-0\" />\n <div>\n <h3 className=\"text-sm font-medium text-red-800\">Error</h3>\n <p className=\"text-sm text-red-700 mt-1\">{error}</p>\n </div>\n </div>\n </div>\n )}\n\n {/* Personal Information */}\n <div className=\"bg-white shadow-sm rounded-xl border border-gray-200\">\n <div className=\"px-6 py-5 border-b border-gray-200\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center space-x-3\">\n <UserIcon className=\"h-6 w-6 text-blue-600\" />\n <h2 className=\"text-xl font-semibold text-gray-900\">Personal Information</h2>\n </div>\n {!editingProfile && (\n <button\n onClick={() => 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 <PencilIcon className=\"h-4 w-4 mr-2\" />\n Edit\n </button>\n )}\n </div>\n </div>\n\n <div className=\"p-6\">\n <div className=\"grid grid-cols-1 gap-8 sm:grid-cols-2\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">First Name</label>\n {editingProfile ? (\n <input\n type=\"text\"\n value={profile.form.firstName}\n onChange={e => 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 <p className=\"text-sm text-gray-900 py-2\">\n {user?.firstName || <span className=\"text-gray-500 italic\">Not provided</span>}\n </p>\n )}\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">Last Name</label>\n {editingProfile ? (\n <input\n type=\"text\"\n value={profile.form.lastName}\n onChange={e => 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 <p className=\"text-sm text-gray-900 py-2\">\n {user?.lastName || <span className=\"text-gray-500 italic\">Not provided</span>}\n </p>\n )}\n </div>\n <div className=\"sm:col-span-2\">\n <label className=\"block text-sm font-medium text-gray-700 mb-3\">\n Email Address\n </label>\n <div className=\"bg-gray-50 rounded-lg p-4 border border-gray-200\">\n <div className=\"flex items-center justify-between\">\n <p className=\"text-base text-gray-900 font-medium\">{user?.email}</p>\n </div>\n <p className=\"text-xs text-gray-500 mt-2\">\n Email cannot be changed from the portal.\n </p>\n </div>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-2\">Phone Number</label>\n {editingProfile ? (\n <input\n type=\"tel\"\n value={profile.form.phone}\n onChange={e => 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 <p className=\"text-sm text-gray-900 py-2\">\n {user?.phone || <span className=\"text-gray-500 italic\">Not provided</span>}\n </p>\n )}\n </div>\n </div>\n\n {editingProfile && (\n <div className=\"flex items-center justify-end space-x-3 pt-6 border-t border-gray-200 mt-6\">\n <button\n onClick={() => 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 <XMarkIcon className=\"h-4 w-4 mr-1\" />\n Cancel\n </button>\n <button\n onClick={async () => {\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 <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\"></div>\n Saving...\n </>\n ) : (\n <>\n <CheckIcon className=\"h-4 w-4 mr-1\" />\n Save Changes\n </>\n )}\n </button>\n </div>\n )}\n </div>\n </div>\n\n {/* Address */}\n <div className=\"bg-white shadow-sm rounded-xl border border-gray-200 mt-8\">\n <div className=\"px-6 py-5 border-b border-gray-200\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center space-x-3\">\n <MapPinIcon className=\"h-6 w-6 text-blue-600\" />\n <h2 className=\"text-xl font-semibold text-gray-900\">Address Information</h2>\n </div>\n {!editingAddress && (\n <button\n onClick={() => 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 <PencilIcon className=\"h-4 w-4 mr-2\" />\n Edit\n </button>\n )}\n </div>\n </div>\n\n <div className=\"p-6\">\n {editingAddress ? (\n <div className=\"space-y-6\">\n <AddressForm\n initialAddress={{\n street: address.form.street,\n streetLine2: address.form.streetLine2,\n city: address.form.city,\n state: address.form.state,\n postalCode: address.form.postalCode,\n country: address.form.country,\n }}\n onChange={a =>\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 <div className=\"flex items-center justify-end space-x-3 pt-2\">\n <button\n onClick={() => 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 <XMarkIcon className=\"h-4 w-4 mr-2\" />\n Cancel\n </button>\n <button\n onClick={async () => {\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 <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\"></div>\n Saving...\n </>\n ) : (\n <>\n <CheckIcon className=\"h-4 w-4 mr-2\" />\n Save Address\n </>\n )}\n </button>\n </div>\n {address.error && (\n <div className=\"bg-red-50 border border-red-200 rounded-xl p-4\">\n <div className=\"flex items-start space-x-3\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-red-500 mt-0.5 flex-shrink-0\" />\n <div>\n <h3 className=\"text-sm font-medium text-red-800\">Address Error</h3>\n <p className=\"text-sm text-red-700 mt-1\">{address.error}</p>\n </div>\n </div>\n </div>\n )}\n </div>\n ) : (\n <div>\n {address.form.street || address.form.city ? (\n <div className=\"bg-gray-50 rounded-lg p-4\">\n <div className=\"text-gray-900 space-y-1\">\n {address.form.street && <p className=\"font-medium\">{address.form.street}</p>}\n {address.form.streetLine2 && <p>{address.form.streetLine2}</p>}\n <p>\n {[address.form.city, address.form.state, address.form.postalCode]\n .filter(Boolean)\n .join(\", \")}\n </p>\n <p>{address.form.country}</p>\n </div>\n </div>\n ) : (\n <div className=\"text-center py-8\">\n <MapPinIcon className=\"h-12 w-12 text-gray-400 mx-auto mb-4\" />\n <p className=\"text-gray-600 mb-4\">No address on file</p>\n <button\n onClick={() => 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 </button>\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n </div>\n </div>\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":240,"column":46,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[6828,6851],"text":"Don&apos;t have an account? "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[6828,6851],"text":"Don&lsquo;t have an account? "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[6828,6851],"text":"Don&#39;t have an account? "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[6828,6851],"text":"Don&rsquo;t have an account? "},"desc":"Replace with `&rsquo;`."}]}],"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<LoginFormData>({\n email: \"\",\n password: \"\",\n rememberMe: false,\n });\n\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<keyof LoginFormData, boolean>>({\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 <form onSubmit={handleSubmit} className={`space-y-6 ${className}`} noValidate>\n {/* General Error */}\n {(errors.general || error) && (\n <ErrorMessage variant=\"default\" className=\"text-center\">\n {errors.general || error}\n </ErrorMessage>\n )}\n\n {/* Email Field */}\n <FormField label=\"Email Address\" error={errors.email} required>\n <Input\n type=\"email\"\n value={formData.email}\n onChange={e => 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 </FormField>\n\n {/* Password Field */}\n <FormField label=\"Password\" error={errors.password} required>\n <Input\n type=\"password\"\n value={formData.password}\n onChange={e => 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 </FormField>\n\n {/* Remember Me */}\n <div className=\"flex items-center justify-between\">\n <label className=\"flex items-center space-x-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={formData.rememberMe}\n onChange={e => handleFieldChange(\"rememberMe\", e.target.checked)}\n disabled={loading}\n className=\"rounded border-gray-300 text-blue-600 focus:ring-blue-500\"\n />\n <span>Remember me</span>\n </label>\n\n {showForgotPasswordLink && (\n <Link\n href=\"/auth/forgot-password\"\n className=\"text-sm text-blue-600 hover:text-blue-500 focus:outline-none focus:underline\"\n >\n Forgot password?\n </Link>\n )}\n </div>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n variant=\"default\"\n size=\"lg\"\n disabled={loading || !isFormValidState}\n loading={loading}\n className=\"w-full\"\n >\n {loading ? \"Signing in...\" : \"Sign In\"}\n </Button>\n\n {/* Signup Link */}\n {showSignupLink && (\n <div className=\"text-center text-sm\">\n <span className=\"text-gray-600\">Don't have an account? </span>\n <Link\n href=\"/auth/signup\"\n className=\"text-blue-600 hover:text-blue-500 focus:outline-none focus:underline font-medium\"\n >\n Sign up\n </Link>\n </div>\n )}\n </form>\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":252,"column":15,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[7393,7442],"text":"\n We&apos;ve sent a password reset link to "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[7393,7442],"text":"\n We&lsquo;ve sent a password reset link to "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[7393,7442],"text":"\n We&#39;ve sent a password reset link to "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[7393,7442],"text":"\n We&rsquo;ve sent a password reset link to "},"desc":"Replace with `&rsquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":258,"column":17,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[7593,7679],"text":"\n Didn&apos;t receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[7593,7679],"text":"\n Didn&lsquo;t receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[7593,7679],"text":"\n Didn&#39;t receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[7593,7679],"text":"\n Didn&rsquo;t receive the email? Check your spam folder or try again.\n "},"desc":"Replace with `&rsquo;`."}]},{"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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":303,"column":46,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we&apos;ll send you a link to reset your password.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we&lsquo;ll send you a link to reset your password.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we&#39;ll send you a link to reset your password.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[8862,8964],"text":"\n Enter your email address and we&rsquo;ll send you a link to reset your password.\n "},"desc":"Replace with `&rsquo;`."}]}],"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<RequestFormData>({\n email: \"\",\n });\n\n const [resetData, setResetData] = useState<ResetFormData>({\n password: \"\",\n confirmPassword: \"\",\n });\n\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<string, boolean>>({});\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 <div className={`text-center space-y-6 ${className}`}>\n <div className=\"space-y-2\">\n <div className=\"w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center\">\n <svg\n className=\"w-8 h-8 text-green-600\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M5 13l4 4L19 7\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-gray-900\">Check your email</h3>\n <p className=\"text-gray-600\">\n We've sent a password reset link to <strong>{requestData.email}</strong>\n </p>\n </div>\n\n <div className=\"space-y-4\">\n <p className=\"text-sm text-gray-500\">\n Didn't receive the email? Check your spam folder or try again.\n </p>\n\n <Button\n variant=\"outline\"\n onClick={() => {\n setIsSubmitted(false);\n setErrors({});\n setTouched({});\n }}\n className=\"w-full\"\n >\n Try Again\n </Button>\n </div>\n\n {showLoginLink && (\n <div className=\"text-sm\">\n <Link\n href=\"/auth/login\"\n className=\"text-blue-600 hover:text-blue-500 focus:outline-none focus:underline\"\n >\n Back to Sign In\n </Link>\n </div>\n )}\n </div>\n );\n }\n\n return (\n <form onSubmit={handleSubmit} className={`space-y-6 ${className}`} noValidate>\n {/* General Error */}\n {(errors.general || error) && (\n <ErrorMessage variant=\"default\" className=\"text-center\">\n {errors.general || error}\n </ErrorMessage>\n )}\n\n {mode === \"request\" ? (\n <>\n {/* Request Mode - Email Input */}\n <div className=\"text-center space-y-2\">\n <h3 className=\"text-lg font-semibold text-gray-900\">Reset your password</h3>\n <p className=\"text-gray-600\">\n Enter your email address and we'll send you a link to reset your password.\n </p>\n </div>\n\n <FormField label=\"Email Address\" error={errors.email} required>\n <Input\n type=\"email\"\n value={requestData.email}\n onChange={e => 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 </FormField>\n\n <Button\n type=\"submit\"\n variant=\"default\"\n size=\"lg\"\n disabled={loading || !requestData.email}\n loading={loading}\n className=\"w-full\"\n >\n {loading ? \"Sending...\" : \"Send Reset Link\"}\n </Button>\n </>\n ) : (\n <>\n {/* Reset Mode - Password Inputs */}\n <div className=\"text-center space-y-2\">\n <h3 className=\"text-lg font-semibold text-gray-900\">Set new password</h3>\n <p className=\"text-gray-600\">Enter your new password below.</p>\n </div>\n\n <FormField label=\"New Password\" error={errors.password} required>\n <Input\n type=\"password\"\n value={resetData.password}\n onChange={e => 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 </FormField>\n\n <FormField label=\"Confirm New Password\" error={errors.confirmPassword} required>\n <Input\n type=\"password\"\n value={resetData.confirmPassword}\n onChange={e => 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 </FormField>\n\n <Button\n type=\"submit\"\n variant=\"default\"\n size=\"lg\"\n disabled={loading || !resetData.password || !resetData.confirmPassword}\n loading={loading}\n className=\"w-full\"\n >\n {loading ? \"Updating...\" : \"Update Password\"}\n </Button>\n </>\n )}\n\n {/* Login Link */}\n {showLoginLink && (\n <div className=\"text-center text-sm\">\n <Link\n href=\"/auth/login\"\n className=\"text-blue-600 hover:text-blue-500 focus:outline-none focus:underline\"\n >\n Back to Sign In\n </Link>\n </div>\n )}\n </form>\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<FormData>({\n email: initialEmail,\n password: \"\",\n confirmPassword: \"\",\n });\n\n const [errors, setErrors] = useState<FormErrors>({});\n const [touched, setTouched] = useState<Record<keyof FormData, boolean>>({\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 <div className={`space-y-6 ${className}`}>\n {/* Header */}\n <div className=\"text-center space-y-2\">\n <h3 className=\"text-lg font-semibold text-gray-900\">Set Your Password</h3>\n <p className=\"text-gray-600\">Complete your account setup by creating a secure password.</p>\n </div>\n\n <form onSubmit={handleSubmit} className=\"space-y-6\" noValidate>\n {/* General Error */}\n {(errors.general || error) && (\n <ErrorMessage variant=\"default\" className=\"text-center\">\n {errors.general || error}\n </ErrorMessage>\n )}\n\n {/* Email Field */}\n <FormField label=\"Email Address\" error={errors.email} required>\n <Input\n type=\"email\"\n value={formData.email}\n onChange={e => 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 <p className=\"mt-1 text-sm text-gray-500\">This email is linked to your WHMCS account</p>\n )}\n </FormField>\n\n {/* Password Field */}\n <FormField label=\"Password\" error={errors.password} required>\n <Input\n type=\"password\"\n value={formData.password}\n onChange={e => 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 <div className=\"mt-2 text-sm text-gray-600\">\n <p>Password requirements:</p>\n <ul className=\"list-disc list-inside space-y-1 text-xs\">\n <li>At least 8 characters long</li>\n <li>Contains uppercase and lowercase letters</li>\n <li>Contains at least one number</li>\n </ul>\n </div>\n </FormField>\n\n {/* Confirm Password Field */}\n <FormField label=\"Confirm Password\" error={errors.confirmPassword} required>\n <Input\n type=\"password\"\n value={formData.confirmPassword}\n onChange={e => 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 </FormField>\n\n {/* Submit Button */}\n <Button\n type=\"submit\"\n variant=\"default\"\n size=\"lg\"\n disabled={loading || !isFormValid}\n loading={loading}\n className=\"w-full\"\n >\n {loading ? \"Setting Password...\" : \"Set Password & Continue\"}\n </Button>\n </form>\n\n {/* Login Link */}\n {showLoginLink && (\n <div className=\"text-center text-sm\">\n <span className=\"text-gray-600\">Already have a password? </span>\n <Link\n href=\"/auth/login\"\n className=\"text-blue-600 hover:text-blue-500 focus:outline-none focus:underline font-medium\"\n >\n Sign in\n </Link>\n </div>\n )}\n </div>\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 `&quot;`, `&ldquo;`, `&#34;`, `&rdquo;`.","line":79,"column":23,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&quot;"},"fix":{"range":[2556,2697],"text":"\n By clicking &quot;Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&quot;`."},{"messageId":"replaceWithAlt","data":{"alt":"&ldquo;"},"fix":{"range":[2556,2697],"text":"\n By clicking &ldquo;Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&ldquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#34;"},"fix":{"range":[2556,2697],"text":"\n By clicking &#34;Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&#34;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rdquo;"},"fix":{"range":[2556,2697],"text":"\n By clicking &rdquo;Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&rdquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `&quot;`, `&ldquo;`, `&#34;`, `&rdquo;`.","line":79,"column":38,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&quot;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account&quot;, you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&quot;`."},{"messageId":"replaceWithAlt","data":{"alt":"&ldquo;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account&ldquo;, you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&ldquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#34;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account&#34;, you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&#34;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rdquo;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account&rdquo;, you'll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&rdquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":79,"column":44,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you&apos;ll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you&lsquo;ll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you&#39;ll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[2556,2697],"text":"\n By clicking \"Create Account\", you&rsquo;ll be able to access your dashboard and start using our\n services immediately.\n "},"desc":"Replace with `&rsquo;`."}]}],"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 <div className=\"space-y-6\">\n <div className=\"space-y-4\">\n <div className=\"flex items-start space-x-3\">\n <input\n type=\"checkbox\"\n id=\"acceptTerms\"\n checked={formData.acceptTerms}\n onChange={e => 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 <div className=\"flex-1\">\n <label htmlFor=\"acceptTerms\" className=\"text-sm text-gray-900\">\n I accept the{\" \"}\n <Link href=\"/terms\" className=\"text-blue-600 hover:text-blue-500 underline\">\n Terms and Conditions\n </Link>{\" \"}\n and{\" \"}\n <Link href=\"/privacy\" className=\"text-blue-600 hover:text-blue-500 underline\">\n Privacy Policy\n </Link>\n <span className=\"text-red-500 ml-1\">*</span>\n </label>\n {errors.acceptTerms && (\n <p className=\"mt-1 text-sm text-red-600\">{errors.acceptTerms}</p>\n )}\n </div>\n </div>\n\n <div className=\"flex items-start space-x-3\">\n <input\n type=\"checkbox\"\n id=\"marketingConsent\"\n checked={formData.marketingConsent}\n onChange={e => 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 <label htmlFor=\"marketingConsent\" className=\"text-sm text-gray-900\">\n I would like to receive marketing communications and product updates\n </label>\n </div>\n </div>\n\n <div className=\"bg-blue-50 border border-blue-200 rounded-lg p-4\">\n <h4 className=\"text-sm font-medium text-blue-900 mb-2\">Almost done!</h4>\n <p className=\"text-sm text-blue-700\">\n By clicking \"Create Account\", you'll be able to access your dashboard and start using our\n services immediately.\n </p>\n </div>\n </div>\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<SignupFormData>({\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<FormErrors>({});\n const [touched, setTouched] = useState<Record<string, boolean>>({});\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 <AccountStep\n formData={formData}\n errors={errors}\n onFieldChange={handleFieldChange}\n onFieldBlur={handleFieldBlur}\n loading={loading}\n />\n ),\n isValid: validateStep(0),\n },\n {\n key: \"personal\",\n title: \"Personal Information\",\n description: \"Tell us about yourself\",\n component: (\n <PersonalStep\n formData={formData}\n errors={errors}\n onFieldChange={handleFieldChange}\n onFieldBlur={handleFieldBlur}\n loading={loading}\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 <AddressStep\n formData={formData}\n errors={errors}\n onFieldChange={handleFieldChange}\n onFieldBlur={handleFieldBlur}\n loading={loading}\n />\n ),\n isValid: validateStep(2),\n },\n {\n key: \"preferences\",\n title: \"Preferences\",\n description: \"Set your preferences\",\n component: (\n <PreferencesStep\n formData={formData}\n errors={errors}\n onFieldChange={handleFieldChange}\n onFieldBlur={handleFieldBlur}\n loading={loading}\n />\n ),\n isValid: validateStep(3),\n },\n ];\n\n return (\n <div className={`space-y-6 ${className}`}>\n {/* General Error */}\n {(errors.general || error) && (\n <ErrorMessage variant=\"default\" className=\"text-center\">\n {errors.general || error}\n </ErrorMessage>\n )}\n\n <MultiStepForm\n steps={steps}\n onSubmit={handleSubmit}\n onStepChange={handleStepChange}\n loading={loading}\n />\n\n {/* Login Link */}\n {showLoginLink && (\n <div className=\"text-center text-sm\">\n <span className=\"text-gray-600\">Already have an account? </span>\n <Link\n href=\"/auth/login\"\n className=\"text-blue-600 hover:text-blue-500 focus:outline-none focus:underline font-medium\"\n >\n Sign in\n </Link>\n </div>\n )}\n </div>\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<void> {\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<AuthUser> {\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<void> {\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<void>;\n signup: (data: SignupRequest) => Promise<void>;\n logout: () => Promise<void>;\n requestPasswordReset: (data: ForgotPasswordRequest) => Promise<void>;\n resetPassword: (data: ResetPasswordRequest) => Promise<void>;\n changePassword: (data: ChangePasswordRequest) => Promise<void>;\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<void>;\n\n // Session management\n checkAuth: () => Promise<void>;\n refreshSession: () => Promise<void>;\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<AuthStoreState>()(\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<string, string | undefined>;\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":192,"column":36,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[4509,4563],"text":"You don&apos;t have the required role to view this content."},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[4509,4563],"text":"You don&lsquo;t have the required role to view this content."},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[4509,4563],"text":"You don&#39;t have the required role to view this content."},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[4509,4563],"text":"You don&rsquo;t have the required role to view this content."},"desc":"Replace with `&rsquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":206,"column":36,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[4864,4911],"text":"You don&apos;t have permission to view this content."},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[4864,4911],"text":"You don&lsquo;t have permission to view this content."},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[4864,4911],"text":"You don&#39;t have permission to view this content."},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[4864,4911],"text":"You don&rsquo;t have permission to view this content."},"desc":"Replace with `&rsquo;`."}]}],"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 = <AuthGuardFallback />,\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 <AuthGuard requireAuth={true} fallback={fallback}>\n {children}\n </AuthGuard>\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 <AuthGuard requireAuth={false} fallback={fallback}>\n {children}\n </AuthGuard>\n );\n}\n\n/**\n * Default fallback component for auth guard\n */\nfunction AuthGuardFallback() {\n return (\n <div className=\"min-h-screen flex items-center justify-center\">\n <div className=\"text-center\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"mt-4 text-gray-600\">Checking authentication...</p>\n </div>\n </div>\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 = <RoleGuardFallback />,\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 = <PermissionGuardFallback />,\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 <div className=\"text-center py-12\">\n <div className=\"text-gray-500\">\n <p className=\"text-lg font-medium\">Access Denied</p>\n <p className=\"mt-2\">You don't have the required role to view this content.</p>\n </div>\n </div>\n );\n}\n\n/**\n * Default fallback for permission guard\n */\nfunction PermissionGuardFallback() {\n return (\n <div className=\"text-center py-12\">\n <div className=\"text-gray-500\">\n <p className=\"text-lg font-medium\">Access Denied</p>\n <p className=\"mt-2\">You don't have permission to view this content.</p>\n </div>\n </div>\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<StatusPillProps, \"variant\" | \"icon\" | \"label\"> {\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: <CheckCircleIcon className=\"h-4 w-4\" />,\n label: \"Paid\",\n };\n case \"overdue\":\n return {\n variant: \"error\" as const,\n icon: <ExclamationTriangleIcon className=\"h-4 w-4\" />,\n label: \"Overdue\",\n };\n case \"unpaid\":\n return {\n variant: \"warning\" as const,\n icon: <ClockIcon className=\"h-4 w-4\" />,\n label: \"Unpaid\",\n };\n case \"cancelled\":\n case \"canceled\":\n return {\n variant: \"neutral\" as const,\n icon: <XCircleIcon className=\"h-4 w-4\" />,\n label: \"Cancelled\",\n };\n case \"draft\":\n return {\n variant: \"neutral\" as const,\n icon: <DocumentTextIcon className=\"h-4 w-4\" />,\n label: \"Draft\",\n };\n case \"refunded\":\n return {\n variant: \"info\" as const,\n icon: <CheckCircleIcon className=\"h-4 w-4\" />,\n label: \"Refunded\",\n };\n case \"collections\":\n return {\n variant: \"error\" as const,\n icon: <ExclamationTriangleIcon className=\"h-4 w-4\" />,\n label: \"Collections\",\n };\n case \"payment pending\":\n return {\n variant: \"warning\" as const,\n icon: <ClockIcon className=\"h-4 w-4\" />,\n label: \"Payment Pending\",\n };\n default:\n return {\n variant: \"neutral\" as const,\n icon: <DocumentTextIcon className=\"h-4 w-4\" />,\n label: status,\n };\n }\n};\n\nconst BillingStatusBadge = forwardRef<HTMLSpanElement, BillingStatusBadgeProps>(\n ({ status, showIcon = true, children, ...props }, ref) => {\n const config = getStatusConfig(status);\n\n return (\n <StatusPill\n ref={ref}\n variant={config.variant}\n icon={showIcon ? config.icon : undefined}\n label={typeof children === \"string\" ? children : config.label}\n {...props}\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<HTMLDivElement> {\n summary: BillingSummaryData;\n loading?: boolean;\n compact?: boolean;\n}\n\nconst BillingSummary = forwardRef<HTMLDivElement, BillingSummaryProps>(\n ({ summary, loading = false, compact = false, className, ...props }, ref) => {\n if (loading) {\n return (\n <div\n ref={ref}\n className={cn(\"bg-white rounded-lg border border-gray-200 p-6\", className)}\n {...props}\n >\n <div className=\"animate-pulse\">\n <div className=\"flex items-center mb-4\">\n <div className=\"w-8 h-8 bg-gray-200 rounded-lg\"></div>\n <div className=\"ml-3 h-6 bg-gray-200 rounded w-32\"></div>\n </div>\n <div className=\"space-y-3\">\n <div className=\"h-4 bg-gray-200 rounded w-full\"></div>\n <div className=\"h-4 bg-gray-200 rounded w-3/4\"></div>\n <div className=\"h-4 bg-gray-200 rounded w-1/2\"></div>\n </div>\n </div>\n </div>\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 <div\n ref={ref}\n className={cn(\n \"bg-white rounded-lg border border-gray-200 transition-all duration-200 hover:shadow-sm\",\n compact ? \"p-4\" : \"p-6\",\n className\n )}\n {...props}\n >\n {/* Header */}\n <div className=\"flex items-center justify-between mb-6\">\n <div className=\"flex items-center\">\n <div className=\"w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center\">\n <CreditCardIcon className=\"w-5 h-5 text-blue-600\" />\n </div>\n <h3 className=\"ml-3 text-lg font-semibold text-gray-900\">Billing Summary</h3>\n </div>\n {!compact && (\n <Link\n href=\"/billing/invoices\"\n className=\"inline-flex items-center text-sm text-blue-600 hover:text-blue-700 font-medium\"\n >\n View All\n <ArrowRightIcon className=\"ml-1 w-4 h-4\" />\n </Link>\n )}\n </div>\n\n {/* Summary Items */}\n <div className={cn(\"space-y-4\", compact && \"space-y-3\")}>\n {summaryItems.map((item, index) => {\n const IconComponent = item.icon;\n\n return (\n <div\n key={index}\n className=\"flex items-center justify-between p-3 rounded-lg bg-gray-50\"\n >\n <div className=\"flex items-center\">\n <IconComponent\n className={cn(\n \"w-5 h-5 mr-3\",\n item.variant === \"error\" && \"text-red-500\",\n item.variant === \"warning\" && \"text-yellow-500\",\n item.variant === \"success\" && \"text-green-500\",\n item.variant === \"neutral\" && \"text-gray-500\"\n )}\n />\n <div>\n <div className=\"text-sm font-medium text-gray-900\">{item.label}</div>\n {!compact && item.count > 0 && (\n <div className=\"text-xs text-gray-500\">\n {item.count} invoice{item.count !== 1 ? \"s\" : \"\"}\n </div>\n )}\n </div>\n </div>\n <div className=\"text-right\">\n <div className=\"text-lg font-semibold text-gray-900\">\n {formatAmount(item.amount)}\n </div>\n {compact && item.count > 0 && (\n <div className=\"text-xs text-gray-500\">\n {item.count} invoice{item.count !== 1 ? \"s\" : \"\"}\n </div>\n )}\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Total Invoices */}\n {!compact && (\n <div className=\"mt-6 pt-4 border-t border-gray-200\">\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-gray-600\">Total Invoices</span>\n <span className=\"font-medium text-gray-900\">{summary.invoiceCount.total}</span>\n </div>\n </div>\n )}\n\n {/* Quick Actions */}\n {compact && (\n <div className=\"mt-4 pt-4 border-t border-gray-200\">\n <Link\n href=\"/billing/invoices\"\n className=\"inline-flex items-center text-sm text-blue-600 hover:text-blue-700 font-medium\"\n >\n View All Invoices\n <ArrowRightIcon className=\"ml-1 w-4 h-4\" />\n </Link>\n </div>\n )}\n </div>\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 <div className=\"px-8 py-6 border-b border-gray-200\">\n <DetailHeader\n title={`Invoice #${invoice.number}`}\n status={{\n label: invoice.status,\n variant:\n invoice.status === \"Paid\"\n ? \"success\"\n : invoice.status === \"Overdue\"\n ? \"error\"\n : invoice.status === \"Unpaid\"\n ? \"warning\"\n : \"neutral\",\n }}\n actions={\n <div className=\"flex flex-col sm:flex-row gap-2 min-w-0\">\n <button\n onClick={onDownload}\n disabled={loadingDownload}\n className=\"inline-flex items-center justify-center px-3 py-2 border border-gray-300 text-xs font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 transition-colors whitespace-nowrap\"\n >\n {loadingDownload ? (\n <LoadingSpinner size=\"sm\" variant=\"gray\" className=\"mr-1.5\" label=\"Downloading...\" />\n ) : (\n <ArrowDownTrayIcon className=\"h-3 w-3 mr-1.5\" />\n )}\n Download\n </button>\n\n {(invoice.status === \"Unpaid\" || invoice.status === \"Overdue\") && (\n <>\n <button\n onClick={onManagePaymentMethods}\n disabled={loadingPaymentMethods}\n className=\"inline-flex items-center justify-center px-3 py-2 border border-gray-300 text-xs font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 transition-colors whitespace-nowrap\"\n >\n {loadingPaymentMethods ? (\n <LoadingSpinner size=\"sm\" variant=\"gray\" className=\"mr-1.5\" label=\"Loading...\" />\n ) : (\n <ServerIcon className=\"h-3 w-3 mr-1.5\" />\n )}\n Payment\n </button>\n\n <button\n onClick={onPay}\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 <LoadingSpinner size=\"sm\" variant=\"white\" className=\"mr-2\" label=\"Processing...\" />\n ) : (\n <ArrowTopRightOnSquareIcon className=\"h-4 w-4 mr-2\" />\n )}\n {invoice.status === \"Overdue\" ? \"Pay Overdue\" : \"Pay Now\"}\n </button>\n </>\n )}\n </div>\n }\n meta={\n <div className=\"flex flex-col sm:flex-row gap-4 text-sm\">\n <div>\n <span className=\"text-gray-500\">Issued:</span>\n <span className=\"ml-2 px-2.5 py-1 text-xs font-bold rounded-md bg-blue-100 text-blue-800 border border-blue-200\">\n {formatDate(invoice.issuedAt)}\n </span>\n </div>\n {invoice.dueDate && (\n <div>\n <span className=\"text-gray-500\">Due:</span>\n <span\n className={`ml-2 px-2.5 py-1 text-xs font-bold rounded-md ${\n invoice.status === \"Overdue\"\n ? \"bg-red-100 text-red-800 border border-red-200\"\n : invoice.status === \"Unpaid\"\n ? \"bg-amber-100 text-amber-800 border border-amber-200\"\n : \"bg-gray-100 text-gray-700 border border-gray-200\"\n }`}\n >\n {formatDate(invoice.dueDate)}\n {invoice.status === \"Overdue\" && \" • OVERDUE\"}\n </span>\n </div>\n )}\n </div>\n }\n />\n </div>\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 <SubCard title=\"Items & Services\">\n {items.length > 0 ? (\n <div className=\"space-y-2\">\n {items.map(item => (\n <div\n key={item.id}\n className=\"flex justify-between items-center py-2 border-b border-gray-100 last:border-b-0\"\n >\n <div className=\"flex-1\">\n <div className=\"font-medium text-gray-900\">{item.description}</div>\n {item.quantity && item.quantity > 1 && (\n <div className=\"text-sm text-gray-500\">Quantity: {item.quantity}</div>\n )}\n {item.serviceId && (\n <div className=\"text-xs text-gray-400\">Service ID: {item.serviceId}</div>\n )}\n </div>\n <div className=\"text-right\">\n <div className=\"font-medium text-gray-900\">\n {formatCurrency(item.amount || 0, { currency })}\n </div>\n </div>\n </div>\n ))}\n </div>\n ) : (\n <div className=\"text-sm text-gray-600\">No items found on this invoice.</div>\n )}\n </SubCard>\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 <SubCard title=\"Totals\">\n <div className=\"max-w-xs ml-auto\">\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm text-gray-600\">\n <span>Subtotal</span>\n <span className=\"font-medium\">{fmt(subtotal)}</span>\n </div>\n {tax > 0 && (\n <div className=\"flex justify-between text-sm text-gray-600\">\n <span>Tax</span>\n <span className=\"font-medium\">{fmt(tax)}</span>\n </div>\n )}\n <div className=\"border-t border-gray-300 pt-2 mt-2\">\n <div className=\"flex justify-between items-center\">\n <span className=\"text-base font-semibold text-gray-900\">Total</span>\n <span className=\"text-2xl font-bold text-gray-900\">{fmt(total)}</span>\n </div>\n </div>\n </div>\n </div>\n </SubCard>\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 <SubCard>\n <div className=\"flex items-center justify-center h-32\">\n <div className=\"text-center space-y-2\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"text-muted-foreground\">Loading invoices...</p>\n </div>\n </div>\n </SubCard>\n );\n }\n\n if (error) {\n return (\n <SubCard>\n <ErrorState\n title=\"Unable to Load Invoices\"\n message={error instanceof Error ? error.message : \"An unexpected error occurred\"}\n variant=\"card\"\n />\n </SubCard>\n );\n }\n\n return (\n <SubCard\n header={\n <SearchFilterBar\n searchValue={searchTerm}\n onSearchChange={setSearchTerm}\n searchPlaceholder=\"Search invoices...\"\n filterValue={statusFilter}\n onFilterChange={value => {\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 <PaginationBar\n currentPage={currentPage}\n pageSize={pageSize}\n totalItems={pagination?.totalItems || 0}\n onPageChange={setCurrentPage}\n />\n ) : undefined\n }\n className={className}\n >\n <InvoiceTable invoices={filtered} loading={isLoading} compact={compact} />\n </SubCard>\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 <CheckCircleIcon className=\"h-5 w-5 text-green-500\" />;\n case \"unpaid\":\n return <ClockIcon className=\"h-5 w-5 text-yellow-500\" />;\n case \"overdue\":\n return <ExclamationTriangleIcon className=\"h-5 w-5 text-red-500\" />;\n case \"cancelled\":\n case \"canceled\":\n return <ExclamationTriangleIcon className=\"h-5 w-5 text-gray-500\" />;\n default:\n return <ClockIcon className=\"h-5 w-5 text-gray-500\" />;\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 <div className=\"flex items-center\">\n {getStatusIcon(invoice.status)}\n <div className=\"ml-3\">\n <div className=\"text-sm font-medium text-gray-900\">{invoice.number}</div>\n {!compact && invoice.description && (\n <div className=\"text-sm text-gray-500 truncate max-w-xs\">{invoice.description}</div>\n )}\n </div>\n </div>\n ),\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (invoice: Invoice) => <BillingStatusBadge status={invoice.status} />,\n },\n {\n key: \"amount\",\n header: \"Amount\",\n render: (invoice: Invoice) => (\n <span className=\"text-sm font-medium text-gray-900\">\n {formatCurrency(invoice.total, {\n currency: invoice.currency,\n currencySymbol: invoice.currencySymbol,\n locale: getCurrencyLocale(invoice.currency),\n })}\n </span>\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 <span className=\"text-sm text-gray-500\">\n {invoice.issuedAt ? format(new Date(invoice.issuedAt), \"MMM d, yyyy\") : \"N/A\"}\n </span>\n ),\n },\n {\n key: \"dueDate\",\n header: \"Due Date\",\n render: (invoice: Invoice) => (\n <span className=\"text-sm text-gray-500\">\n {invoice.dueDate ? format(new Date(invoice.dueDate), \"MMM d, yyyy\") : \"N/A\"}\n </span>\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 <div className=\"flex items-center justify-end space-x-2\">\n <Link\n href={`/billing/invoices/${invoice.id}`}\n className=\"text-blue-600 hover:text-blue-900 text-sm font-medium\"\n onClick={e => e.stopPropagation()}\n >\n View\n </Link>\n <ArrowTopRightOnSquareIcon className=\"h-4 w-4 text-gray-400\" />\n </div>\n ),\n });\n }\n\n return baseColumns;\n }, [compact, showActions]);\n\n const emptyState = {\n icon: <DocumentTextIcon className=\"h-12 w-12\" />,\n title: \"No invoices found\",\n description: \"No invoices have been generated yet.\",\n };\n\n if (loading) {\n return (\n <div className=\"animate-pulse\">\n <div className=\"space-y-3\">\n {[...Array(5)].map((_, i) => (\n <div key={i} className=\"h-16 bg-gray-200 rounded\"></div>\n ))}\n </div>\n </div>\n );\n }\n\n return (\n <DataTable\n data={invoices}\n columns={columns}\n emptyState={emptyState}\n onRowClick={handleInvoiceClick}\n className={cn(\"invoice-table\", className)}\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={<CreditCardIcon·/>}·title=\"Invoice\"·description=\"Invoice·details·and·actions\"` with `⏎········icon={<CreditCardIcon·/>}⏎········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={<CreditCardIcon />}\n title=\"Invoice\"\n description=\"Invoice details and actions\"\n "}},{"ruleId":"prettier/prettier","severity":1,"message":"Replace `·icon={<CreditCardIcon·/>}·title=\"Invoice\"·description=\"Invoice·details·and·actions\"` with `⏎········icon={<CreditCardIcon·/>}⏎········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={<CreditCardIcon />}\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 <PageLayout icon={<CreditCardIcon />} title=\"Invoice\" description=\"Invoice details and actions\">\n <div className=\"flex items-center justify-center h-64\">\n <div className=\"text-center space-y-3\">\n <LoadingSpinner size=\"xl\" />\n <p className=\"text-gray-600\">Loading invoice...</p>\n </div>\n </div>\n </PageLayout>\n );\n }\n\n if (error || !invoice) {\n return (\n <PageLayout icon={<CreditCardIcon />} title=\"Invoice\" description=\"Invoice details and actions\">\n <ErrorState\n title=\"Error loading invoice\"\n message={error instanceof Error ? error.message : \"Invoice not found\"}\n variant=\"page\"\n />\n <div className=\"mt-4\">\n <Link href=\"/billing/invoices\" className=\"text-primary font-medium\">\n ← Back to invoices\n </Link>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <div className=\"py-8\">\n <div className=\"max-w-4xl mx-auto px-4 sm:px-6 md:px-8\">\n <div className=\"mb-6\">\n <Link href=\"/billing/invoices\" className=\"inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors\">\n ← Back to Invoices\n </Link>\n </div>\n\n <div className=\"bg-white rounded-2xl shadow border\">\n <InvoiceHeader\n invoice={invoice}\n loadingDownload={loadingDownload}\n loadingPayment={loadingPayment}\n loadingPaymentMethods={loadingPaymentMethods}\n onDownload={() => handleCreateSsoLink(\"download\")}\n onPay={() => handleCreateSsoLink(\"pay\")}\n onManagePaymentMethods={handleManagePaymentMethods}\n />\n\n {invoice.status === \"Paid\" && (\n <div className=\"px-8 mt-6 flex items-center text-green-700 bg-green-50 border border-green-200 mx-8 rounded-lg py-3\">\n <CheckCircleIcon className=\"h-5 w-5 mr-3\" />\n <div className=\"text-sm\">\n <span className=\"font-semibold\">Invoice Paid</span>\n <span className=\"ml-2\">• Paid on {invoice.paidDate || invoice.issuedAt}</span>\n </div>\n </div>\n )}\n\n <div className=\"px-8 py-6 space-y-6\">\n <InvoiceItems items={invoice.items} currency={invoice.currency} />\n <InvoiceTotals subtotal={invoice.subtotal} tax={invoice.tax} total={invoice.total} currency={invoice.currency} />\n\n {(invoice.status === \"Unpaid\" || invoice.status === \"Overdue\") && (\n <SubCard title=\"Payment\">\n <div className=\"flex flex-wrap gap-2\">\n <button\n onClick={handleManagePaymentMethods}\n disabled={loadingPaymentMethods}\n className=\"inline-flex items-center justify-center px-3 py-2 border border-gray-300 text-xs font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 transition-colors whitespace-nowrap\"\n >\n {loadingPaymentMethods ? (\n <div className=\"animate-spin rounded-full h-3 w-3 border-b-2 border-gray-600 mr-1.5\"></div>\n ) : (\n <span className=\"mr-1.5\">💳</span>\n )}\n Payment Methods\n </button>\n <button\n onClick={() => 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 <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2\"></div>\n ) : (\n <span className=\"mr-2\">↗</span>\n )}\n {invoice.status === \"Overdue\" ? \"Pay Overdue\" : \"Pay Now\"}\n </button>\n </div>\n </SubCard>\n )}\n </div>\n </div>\n </div>\n </div>\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={<CreditCardIcon·/>}·title=\"Invoices\"·description=\"Manage·and·view·your·billing·invoices\"` with `⏎······icon={<CreditCardIcon·/>}⏎······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={<CreditCardIcon />}\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 <PageLayout icon={<CreditCardIcon />} title=\"Invoices\" description=\"Manage and view your billing invoices\">\n <InvoicesList />\n </PageLayout>\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":149,"column":28,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[5712,5823],"text":"\n You haven&apos;t added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[5712,5823],"text":"\n You haven&lsquo;t added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[5712,5823],"text":"\n You haven&#39;t added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[5712,5823],"text":"\n You haven&rsquo;t added any payment methods yet. Add one to make payments easier.\n "},"desc":"Replace with `&rsquo;`."}]}],"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<string | null>(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 <PageLayout\n icon={<CreditCardIcon />}\n title=\"Payment Methods\"\n description=\"Manage your saved payment methods and billing information\"\n >\n <ErrorState\n title=\"Unable to Access Payment Methods\"\n message={errorMessage}\n variant=\"page\"\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n icon={<CreditCardIcon />}\n title=\"Payment Methods\"\n description=\"Manage your saved payment methods and billing information\"\n >\n <InlineToast\n visible={paymentRefresh.toast.visible}\n text={paymentRefresh.toast.text}\n tone={paymentRefresh.toast.tone}\n />\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-6\">\n <div className=\"lg:col-span-2\">\n {isLoadingPaymentMethods ? (\n <SubCard>\n <div className=\"flex items-center justify-center h-32\">\n <div className=\"text-center space-y-4\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"text-muted-foreground\">Loading payment methods...</p>\n </div>\n </div>\n </SubCard>\n ) : paymentMethodsData && paymentMethodsData.paymentMethods.length > 0 ? (\n <SubCard\n header={\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-semibold text-gray-900\">Your Payment Methods</h2>\n <button\n onClick={() => {\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 <PlusIcon className=\"w-4 h-4\" />\n Add New\n </button>\n </div>\n }\n >\n <div className=\"space-y-4\">\n {paymentMethodsData.paymentMethods.map(paymentMethod => (\n <PaymentMethodCard\n key={paymentMethod.id}\n paymentMethod={paymentMethod}\n onEdit={() => {\n void openPaymentMethods();\n }}\n onDelete={() => {\n void openPaymentMethods();\n }}\n onSetDefault={() => {\n void openPaymentMethods();\n }}\n />\n ))}\n </div>\n </SubCard>\n ) : (\n <SubCard>\n <div className=\"text-center py-12\">\n <div className=\"mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center mb-6\">\n <CreditCardIcon className=\"w-8 h-8 text-blue-600\" />\n </div>\n <h2 className=\"text-xl font-semibold text-gray-900 mb-2\">No Payment Methods</h2>\n <p className=\"text-gray-600 mb-8\">\n You haven't added any payment methods yet. Add one to make payments easier.\n </p>\n <button\n onClick={() => {\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 <div className=\"animate-spin rounded-full h-4 w-4 border-b-2 border-white\"></div>\n Opening...\n </>\n ) : (\n <>\n <PlusIcon className=\"w-4 h-4\" />\n Add Payment Method\n </>\n )}\n </button>\n <p className=\"text-sm text-gray-500 mt-4\">Opens in a new tab for security</p>\n </div>\n </SubCard>\n )}\n </div>\n\n <div className=\"space-y-6\">\n <div className=\"bg-blue-50 rounded-lg p-4\">\n <div className=\"flex items-start\">\n <div className=\"flex-shrink-0\">\n <CreditCardIcon className=\"h-5 w-5 text-blue-400\" />\n </div>\n <div className=\"ml-3\">\n <h3 className=\"text-sm font-medium text-blue-800\">Secure & Encrypted</h3>\n <p className=\"text-sm text-blue-700 mt-1\">\n All payment information is securely encrypted and protected with industry-standard\n security.\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"bg-gray-50 rounded-lg p-4\">\n <h3 className=\"text-sm font-medium text-gray-800 mb-2\">Supported Payment Methods</h3>\n <ul className=\"text-sm text-gray-600 space-y-1\">\n <li>• Credit Cards (Visa, MasterCard, American Express)</li>\n <li>• Debit Cards</li>\n </ul>\n </div>\n </div>\n </div>\n </PageLayout>\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<InvoiceList> {\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<InvoiceList>(`/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<Invoice> {\n const res = await apiClient.get<Invoice>(`/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<Subscription[]> {\n const res = await apiClient.get<Subscription[]>(`/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<InvoiceSsoLink> {\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<InvoiceSsoLink>(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<InvoicePaymentLink> {\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<InvoicePaymentLink>(url);\n return res.data as InvoicePaymentLink;\n }\n\n /**\n * Fetch user's payment methods\n */\n static async getPaymentMethods(): Promise<PaymentMethodList> {\n const res = await apiClient.get<PaymentMethodList>(\"/invoices/payment-methods\");\n return res.data as PaymentMethodList;\n }\n\n /**\n * Fetch available payment gateways\n */\n static async getPaymentGateways(): Promise<PaymentGatewayList> {\n const res = await apiClient.get<PaymentGatewayList>(\"/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<PaymentMethod> {\n const res = await apiClient.patch<PaymentMethod>(\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<void> {\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 <AnimatedCard variant={getCardVariant()} className={`p-6 ${isDisabled ? \"opacity-60\" : \"\"}`}>\n {/* Step Header */}\n <div className=\"mb-6\">\n <div className=\"flex items-start gap-4\">\n {/* Step Indicator */}\n {showStepIndicator && (\n <div\n className={`flex-shrink-0 w-10 h-10 rounded-full border-2 flex items-center justify-center font-bold transition-all duration-300 ${getStepIndicatorClasses()}`}\n >\n {isCompleted ? <CheckCircleIcon className=\"w-6 h-6\" /> : <span>{stepNumber}</span>}\n </div>\n )}\n\n {/* Step Title and Description */}\n <div className=\"flex-1\">\n <h3\n className={`text-xl font-bold mb-2 ${isDisabled ? \"text-gray-500\" : \"text-gray-900\"}`}\n >\n {title}\n </h3>\n {description && (\n <p\n className={`text-sm leading-relaxed ${isDisabled ? \"text-gray-400\" : \"text-gray-600\"}`}\n >\n {description}\n </p>\n )}\n\n {/* Validation Status */}\n {validation && (\n <div className=\"mt-3\">\n {hasErrors && (\n <div className=\"flex items-start gap-2 text-red-600\">\n <ExclamationTriangleIcon className=\"h-4 w-4 flex-shrink-0 mt-0.5\" />\n <div className=\"text-sm\">\n {validation.errors!.map((error, index) => (\n <div key={index}>{error}</div>\n ))}\n </div>\n </div>\n )}\n\n {hasWarnings && !hasErrors && (\n <div className=\"flex items-start gap-2 text-amber-600\">\n <ExclamationTriangleIcon className=\"h-4 w-4 flex-shrink-0 mt-0.5\" />\n <div className=\"text-sm\">\n {validation.warnings!.map((warning, index) => (\n <div key={index}>{warning}</div>\n ))}\n </div>\n </div>\n )}\n\n {isValid && !hasWarnings && isCompleted && (\n <div className=\"flex items-center gap-2 text-green-600\">\n <CheckCircleIcon className=\"h-4 w-4\" />\n <span className=\"text-sm font-medium\">Configuration complete</span>\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n\n {headerContent && <div className=\"mt-4\">{headerContent}</div>}\n </div>\n\n {/* Step Content */}\n {!isDisabled && <div className=\"mb-6\">{children}</div>}\n\n {/* Help Text */}\n {helpText && !isDisabled && (\n <div className=\"mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200\">\n <div className=\"flex items-start gap-2\">\n <InformationCircleIcon className=\"h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5\" />\n <p className=\"text-sm text-blue-700\">{helpText}</p>\n </div>\n </div>\n )}\n\n {/* Info Text */}\n {infoText && !isDisabled && (\n <div className=\"mb-6 p-3 bg-gray-50 rounded-lg border border-gray-200\">\n <p className=\"text-sm text-gray-600\">{infoText}</p>\n </div>\n )}\n\n {/* Actions */}\n {showActions && !isDisabled && (\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"flex gap-3 flex-1\">\n {onPrevious && (\n <Button\n onClick={onPrevious}\n variant=\"outline\"\n className=\"flex-1 sm:flex-none\"\n disabled={disabled || loading}\n >\n {previousLabel}\n </Button>\n )}\n\n {onSkip && (\n <Button\n onClick={onSkip}\n variant=\"outline\"\n className=\"flex-1 sm:flex-none\"\n disabled={disabled || loading}\n >\n {skipLabel}\n </Button>\n )}\n </div>\n\n {onNext && (\n <Button\n onClick={onNext}\n className=\"flex-1 sm:flex-none sm:min-w-[120px]\"\n disabled={disabled || loading || hasErrors}\n >\n {loading ? (\n <span className=\"flex items-center justify-center\">\n <svg\n className=\"animate-spin -ml-1 mr-3 h-4 w-4 text-white\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n ></circle>\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n Processing...\n </span>\n ) : (\n nextLabel\n )}\n </Button>\n )}\n </div>\n )}\n\n {/* Footer Content */}\n {footerContent && <div className=\"mt-6 pt-4 border-t border-gray-200\">{footerContent}</div>}\n </AnimatedCard>\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 <AnimatedCard className={`${getVariantClasses()} ${sizeClasses[size]} overflow-hidden`}>\n {/* Header */}\n <div className=\"mb-6\">\n <div className=\"flex items-center justify-between mb-4\">\n <h3 className=\"text-xl font-bold text-gray-900\">Order Summary</h3>\n {variant === \"checkout\" && (\n <div className=\"text-right\">\n <div className=\"text-2xl font-bold text-blue-600\">\n ¥{formatPrice(totals.monthlyTotal)}/mo\n </div>\n {totals.oneTimeTotal > 0 && (\n <div className=\"text-sm text-orange-600 font-medium\">\n + ¥{formatPrice(totals.oneTimeTotal)} one-time\n </div>\n )}\n </div>\n )}\n </div>\n\n {headerContent}\n </div>\n\n {/* Plan Information */}\n <div className=\"mb-6 pb-4 border-b border-gray-200\">\n <div className=\"flex justify-between items-start mb-2\">\n <div>\n <h4 className=\"font-semibold text-gray-900\">\n {planName}\n {planTier && <span className=\"text-blue-600 ml-2\">({planTier})</span>}\n </h4>\n {planDescription && <p className=\"text-sm text-gray-600 mt-1\">{planDescription}</p>}\n </div>\n </div>\n\n {/* Configuration Details */}\n {configurations.length > 0 && (\n <div className=\"mt-4 space-y-2\">\n {configurations.map((config, index) => (\n <div key={index} className=\"flex justify-between text-sm\">\n <span className={`text-gray-600 ${config.important ? \"font-medium\" : \"\"}`}>\n {config.label}:\n </span>\n <span\n className={`${config.important ? \"font-semibold text-gray-900\" : \"text-gray-800\"}`}\n >\n {config.value}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n\n {/* Detailed Pricing Breakdown */}\n {showDetailedBreakdown && variant !== \"simple\" && (\n <div className=\"mb-6\">\n {/* Monthly Services */}\n {monthlyItems.length > 0 && (\n <div className=\"mb-4\">\n <h5 className=\"font-medium text-gray-900 mb-2\">Monthly Charges</h5>\n <div className=\"space-y-2\">\n {monthlyItems.map((item, index) => (\n <div key={index} className=\"flex justify-between text-sm\">\n <div className=\"flex-1\">\n <span className=\"text-gray-700\">{item.name}</span>\n {item.isAutoAdded && (\n <span className=\"text-xs text-blue-600 ml-2\">(Auto-added)</span>\n )}\n {item.description && (\n <div className=\"text-xs text-gray-500 mt-1\">{item.description}</div>\n )}\n </div>\n <span className=\"font-medium text-gray-900 ml-4\">\n ¥{formatPrice(item.price || 0)}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* One-time Charges */}\n {oneTimeItems.length > 0 && (\n <div className=\"mb-4\">\n <h5 className=\"font-medium text-gray-900 mb-2\">One-time Charges</h5>\n <div className=\"space-y-2\">\n {oneTimeItems.map((item, index) => (\n <div key={index} className=\"flex justify-between text-sm\">\n <div className=\"flex-1\">\n <span className=\"text-gray-700\">{item.name}</span>\n {item.description && (\n <div className=\"text-xs text-gray-500 mt-1\">{item.description}</div>\n )}\n </div>\n <span className=\"font-medium text-orange-600 ml-4\">\n ¥{formatPrice(item.price || 0)}\n </span>\n </div>\n ))}\n </div>\n </div>\n )}\n\n {/* Discounts */}\n {showDiscounts && totals.discountAmount && totals.discountAmount > 0 && (\n <div className=\"mb-4\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-green-700\">Discount Applied</span>\n <span className=\"font-medium text-green-600\">\n -¥{formatPrice(totals.discountAmount)}\n </span>\n </div>\n </div>\n )}\n\n {/* Taxes */}\n {showTaxes && totals.taxAmount && totals.taxAmount > 0 && (\n <div className=\"mb-4\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-gray-700\">Tax (10%)</span>\n <span className=\"font-medium text-gray-900\">¥{formatPrice(totals.taxAmount)}</span>\n </div>\n </div>\n )}\n </div>\n )}\n\n {/* Simple Item List for simple variant */}\n {variant === \"simple\" && (\n <div className=\"mb-6 space-y-2\">\n {orderItems.map((item, index) => (\n <div key={index} className=\"flex justify-between text-sm\">\n <span className=\"text-gray-700\">{item.name}</span>\n <span className=\"font-medium\">\n ¥{formatPrice(item.price || 0)}\n {item.billingCycle === \"Monthly\" ? \"/mo\" : \" one-time\"}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Totals */}\n <div className=\"mb-6 pt-4 border-t border-gray-300\">\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-lg font-semibold\">\n <span className=\"text-gray-900\">Monthly Total:</span>\n <span className=\"text-blue-600\">¥{formatPrice(totals.monthlyTotal)}</span>\n </div>\n\n {totals.oneTimeTotal > 0 && (\n <div className=\"flex justify-between text-lg font-semibold\">\n <span className=\"text-gray-900\">One-time Total:</span>\n <span className=\"text-orange-600\">¥{formatPrice(totals.oneTimeTotal)}</span>\n </div>\n )}\n\n {totals.annualTotal && (\n <div className=\"flex justify-between text-sm text-gray-600\">\n <span>Annual Total:</span>\n <span>¥{formatPrice(totals.annualTotal)}</span>\n </div>\n )}\n </div>\n </div>\n\n {/* Info Lines */}\n {infoLines.length > 0 && (\n <div className=\"mb-6 p-4 bg-blue-50 rounded-lg border border-blue-200\">\n <div className=\"flex items-start gap-2\">\n <InformationCircleIcon className=\"h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5\" />\n <div className=\"space-y-1\">\n {infoLines.map((line, index) => (\n <p key={index} className=\"text-sm text-blue-700\">\n {line}\n </p>\n ))}\n </div>\n </div>\n </div>\n )}\n\n {/* Disclaimers */}\n {disclaimers.length > 0 && (\n <div className=\"mb-6 p-4 bg-gray-50 rounded-lg border border-gray-200\">\n <div className=\"space-y-2\">\n {disclaimers.map((disclaimer, index) => (\n <p key={index} className=\"text-xs text-gray-600\">\n {disclaimer}\n </p>\n ))}\n </div>\n </div>\n )}\n\n {/* Custom Content */}\n {children && <div className=\"mb-6\">{children}</div>}\n\n {/* Actions */}\n {showActions && (\n <div className=\"flex gap-4\">\n {backUrl ? (\n <Button\n as=\"a\"\n href={backUrl}\n variant=\"outline\"\n className=\"flex-1 group\"\n disabled={disabled || loading}\n >\n <ArrowLeftIcon className=\"w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform duration-300\" />\n {backLabel}\n </Button>\n ) : onBack ? (\n <Button\n onClick={onBack}\n variant=\"outline\"\n className=\"flex-1 group\"\n disabled={disabled || loading}\n >\n <ArrowLeftIcon className=\"w-4 h-4 mr-2 group-hover:-translate-x-1 transition-transform duration-300\" />\n {backLabel}\n </Button>\n ) : null}\n\n {onContinue && (\n <Button onClick={onContinue} className=\"flex-1 group\" disabled={disabled || loading}>\n {loading ? (\n <span className=\"flex items-center justify-center\">\n <svg\n className=\"animate-spin -ml-1 mr-3 h-4 w-4 text-white\"\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n >\n <circle\n className=\"opacity-25\"\n cx=\"12\"\n cy=\"12\"\n r=\"10\"\n stroke=\"currentColor\"\n strokeWidth=\"4\"\n ></circle>\n <path\n className=\"opacity-75\"\n fill=\"currentColor\"\n d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"\n ></path>\n </svg>\n Processing...\n </span>\n ) : (\n <>\n {continueLabel}\n <ArrowRightIcon className=\"w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300\" />\n </>\n )}\n </Button>\n )}\n </div>\n )}\n\n {/* Footer Content */}\n {footerContent && <div className=\"mt-6 pt-4 border-t border-gray-200\">{footerContent}</div>}\n </AnimatedCard>\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<string>(selectedMethodId || \"\");\n const [errors, setErrors] = useState<string[]>([]);\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 <CreditCardIcon className=\"h-6 w-6 text-gray-400\" />;\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 <div className={containerClasses}>\n <div className=\"flex items-center justify-center py-8\">\n <LoadingSpinner size=\"lg\" />\n <span className=\"ml-3 text-gray-600\">Loading payment methods...</span>\n </div>\n </div>\n );\n }\n\n return (\n <div className={containerClasses}>\n {showTitle && (\n <div className=\"mb-6\">\n <div className=\"flex items-center gap-2 mb-2\">\n <CreditCardIcon className=\"h-5 w-5 text-blue-600\" />\n <h3 className=\"text-lg font-semibold text-gray-900\">{title}</h3>\n </div>\n {description && <p className=\"text-sm text-gray-600\">{description}</p>}\n </div>\n )}\n\n {/* No payment methods */}\n {existingMethods.length === 0 ? (\n <div className=\"text-center py-8\">\n <div className=\"w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4\">\n <CreditCardIcon className=\"h-8 w-8 text-gray-400\" />\n </div>\n <h4 className=\"text-lg font-medium text-gray-900 mb-2\">No Payment Method on File</h4>\n <p className=\"text-gray-600 mb-6\">Add a payment method to complete your order.</p>\n {allowNewMethod && onAddNewMethod && (\n <button\n onClick={onAddNewMethod}\n disabled={disabled}\n className=\"bg-blue-600 text-white px-6 py-2.5 rounded-lg hover:bg-blue-700 transition-colors font-medium shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n Add Payment Method\n </button>\n )}\n </div>\n ) : (\n <div className=\"space-y-4\">\n {/* Existing payment methods */}\n <div className=\"space-y-3\">\n {existingMethods.map(method => (\n <label\n key={method.id}\n className={`flex items-center p-4 border-2 rounded-lg cursor-pointer transition-all ${\n selectedMethod === method.id\n ? \"border-blue-500 bg-blue-50 ring-2 ring-blue-100\"\n : \"border-gray-200 hover:border-gray-300 bg-white\"\n } ${disabled ? \"opacity-50 cursor-not-allowed\" : \"\"}`}\n >\n <input\n type=\"radio\"\n name=\"paymentMethod\"\n value={method.id}\n checked={selectedMethod === method.id}\n onChange={() => handleMethodSelect(method.id)}\n disabled={disabled}\n className=\"text-blue-600 focus:ring-blue-500 mr-4\"\n />\n\n <div className=\"flex items-center flex-1\">\n <div className=\"mr-4\">{getCardBrandIcon(method.brand)}</div>\n\n <div className=\"flex-1\">\n <div className=\"flex items-center gap-2\">\n <span className=\"font-medium text-gray-900\">\n {method.brand?.toUpperCase()} {formatCardNumber(method.last4)}\n </span>\n {method.isDefault && (\n <span className=\"bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium\">\n Default\n </span>\n )}\n </div>\n\n <div className=\"text-sm text-gray-600 mt-1\">\n {method.name && <span>{method.name} • </span>}\n Expires {formatExpiry(method.expiryMonth, method.expiryYear)}\n </div>\n </div>\n\n {selectedMethod === method.id && (\n <CheckCircleIcon className=\"h-5 w-5 text-blue-600 ml-4\" />\n )}\n </div>\n </label>\n ))}\n </div>\n\n {/* Add new payment method option */}\n {allowNewMethod && onAddNewMethod && (\n <div className=\"pt-4 border-t border-gray-200\">\n <button\n onClick={onAddNewMethod}\n disabled={disabled}\n className=\"w-full flex items-center justify-center gap-2 p-4 border-2 border-dashed border-gray-300 rounded-lg text-gray-600 hover:border-gray-400 hover:text-gray-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <CreditCardIcon className=\"h-5 w-5\" />\n <span className=\"font-medium\">Add New Payment Method</span>\n </button>\n </div>\n )}\n </div>\n )}\n\n {/* Custom content */}\n {children && <div className=\"mt-6\">{children}</div>}\n\n {/* Validation errors */}\n {errors.length > 0 && (\n <div className=\"mt-4 p-3 bg-red-50 border border-red-200 rounded-lg\">\n <div className=\"flex items-start gap-2\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-red-500 flex-shrink-0 mt-0.5\" />\n <div>\n <p className=\"text-sm font-medium text-red-800\">Payment Required</p>\n <ul className=\"mt-1 text-sm text-red-700\">\n {errors.map((error, index) => (\n <li key={index}>{error}</li>\n ))}\n </ul>\n </div>\n </div>\n </div>\n )}\n\n {/* Footer content */}\n {footerContent && <div className=\"mt-6 pt-4 border-t border-gray-200\">{footerContent}</div>}\n </div>\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 <AnimatedCard\n variant={variant}\n className={`overflow-hidden flex flex-col h-full ${sizeClasses[size]}`}\n onClick={onClick}\n disabled={disabled}\n >\n {/* Header with badge and icon */}\n <div className=\"flex items-start justify-between mb-4\">\n <div className=\"flex items-center gap-3\">\n {icon && <div className=\"flex-shrink-0\">{icon}</div>}\n <div className=\"flex flex-col gap-2\">\n {badge && (\n <span\n className={`px-3 py-1 rounded-full text-sm font-medium border ${getBadgeClasses(badge.variant)}`}\n >\n {badge.text}\n </span>\n )}\n </div>\n </div>\n\n {/* Pricing display */}\n {(monthlyPrice || oneTimePrice) && (\n <div className=\"text-right flex-shrink-0\">\n {monthlyPrice && (\n <div className=\"flex items-baseline justify-end gap-1 text-2xl font-bold text-gray-900\">\n <CurrencyYenIcon className=\"h-6 w-6\" />\n <span>{monthlyPrice.toLocaleString()}</span>\n <span className=\"text-sm text-gray-500 font-normal whitespace-nowrap\">/month</span>\n </div>\n )}\n {oneTimePrice && (\n <div className=\"flex items-baseline justify-end gap-1 text-lg font-semibold text-orange-600 mt-1\">\n <CurrencyYenIcon className=\"h-4 w-4\" />\n <span>{oneTimePrice.toLocaleString()}</span>\n <span className=\"text-xs text-orange-500 font-normal\">one-time</span>\n </div>\n )}\n </div>\n )}\n </div>\n\n {/* Product name and description */}\n <div className=\"mb-4\">\n <h3 className=\"text-xl font-semibold text-gray-900 mb-2\">{name}</h3>\n {description && <p className=\"text-gray-600 text-sm leading-relaxed\">{description}</p>}\n </div>\n\n {/* Features list */}\n {features.length > 0 && (\n <div className=\"mb-6 flex-grow\">\n <h4 className=\"font-medium text-gray-900 mb-3\">Features:</h4>\n <ul className=\"space-y-2 text-sm text-gray-700\">\n {features.map((feature, index) => (\n <li key={index} className=\"flex items-start\">\n <span className=\"text-green-600 mr-2 flex-shrink-0\">✓</span>\n <span>{feature}</span>\n </li>\n ))}\n </ul>\n </div>\n )}\n\n {/* Custom children content */}\n {children && <div className=\"mb-4 flex-grow\">{children}</div>}\n\n {/* Action button */}\n <div className=\"mt-auto\">\n {href ? (\n <Button as=\"a\" href={href} className=\"w-full group\" disabled={disabled}>\n <span>{actionLabel}</span>\n <ArrowRightIcon className=\"w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300\" />\n </Button>\n ) : onClick ? (\n <Button onClick={onClick} className=\"w-full group\" disabled={disabled}>\n <span>{actionLabel}</span>\n <ArrowRightIcon className=\"w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300\" />\n </Button>\n ) : null}\n </div>\n\n {/* Custom footer */}\n {footer && <div className=\"mt-4 pt-4 border-t border-gray-200\">{footer}</div>}\n </AnimatedCard>\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 <AnimatedCard className=\"text-center p-6 rounded-2xl\">\n <div className=\"flex justify-center mb-6\">\n <div className=\"p-3 bg-gray-50 rounded-xl\">{icon}</div>\n </div>\n <h3 className=\"text-xl font-bold text-gray-900 mb-3\">{title}</h3>\n <p className=\"text-gray-600 leading-relaxed\">{description}</p>\n </AnimatedCard>\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 <AnimatedCard className=\"relative group rounded-3xl overflow-hidden h-full p-0\">\n <div className=\"p-8 h-full flex flex-col\">\n <div className=\"flex items-center gap-4 mb-6\">\n <div className={`p-4 rounded-xl ${colors.iconBg}`}>\n <div className={colors.iconText}>{icon}</div>\n </div>\n <div>\n <h3 className=\"text-2xl font-bold text-gray-900\">{title}</h3>\n </div>\n </div>\n\n <p className=\"text-gray-600 mb-6 leading-relaxed\">{description}</p>\n\n <ul className=\"space-y-3 mb-8 flex-grow\">\n {features.map((feature, index) => (\n <li key={index} className=\"flex items-center gap-3\">\n <div className={`w-2 h-2 rounded-full ${colors.button.split(\" \")[0]}`} />\n <span className=\"text-sm text-gray-700\">{feature}</span>\n </li>\n ))}\n </ul>\n\n <div className=\"mt-auto relative z-10\">\n <Button as=\"a\" href={href} className=\"w-full font-semibold rounded-2xl relative z-10 group\" size=\"lg\">\n <span>Explore Plans</span>\n <ArrowRightIcon className=\"w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform\" />\n </Button>\n </div>\n </div>\n <div className={`absolute inset-0 ${colors.bg} opacity-0 group-hover:opacity-10 transition-opacity duration-300 pointer-events-none`} />\n </AnimatedCard>\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 `&quot;`, `&ldquo;`, `&#34;`, `&rdquo;`.","line":185,"column":57,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&quot;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as &quot;Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `&quot;`."},{"messageId":"replaceWithAlt","data":{"alt":"&ldquo;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as &ldquo;Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `&ldquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#34;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as &#34;Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `&#34;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rdquo;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as &rdquo;Platinum Base Plan\". Device subscriptions\n will be added later.\n "},"desc":"Replace with `&rdquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `&quot;`, `&ldquo;`, `&#34;`, `&rdquo;`.","line":185,"column":76,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&quot;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan&quot;. Device subscriptions\n will be added later.\n "},"desc":"Replace with `&quot;`."},{"messageId":"replaceWithAlt","data":{"alt":"&ldquo;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan&ldquo;. Device subscriptions\n will be added later.\n "},"desc":"Replace with `&ldquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#34;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan&#34;. Device subscriptions\n will be added later.\n "},"desc":"Replace with `&#34;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rdquo;"},"fix":{"range":[6708,6875],"text":"\n * Will appear on the invoice as \"Platinum Base Plan&rdquo;. Device subscriptions\n will be added later.\n "},"desc":"Replace with `&rdquo;`."}]}],"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 <PageLayout\n icon={<ServerIcon />}\n title=\"Configure Internet Service\"\n description=\"Set up your internet service options\"\n >\n <div className=\"flex items-center justify-center py-12\">\n <LoadingSpinner size=\"lg\" label=\"Loading configuration...\" />\n </div>\n </PageLayout>\n );\n }\n\n if (!plan) {\n return (\n <PageLayout\n icon={<ServerIcon />}\n title=\"Configure Internet Service\"\n description=\"Set up your internet service options\"\n >\n <div className=\"text-center py-12\">\n <p className=\"text-gray-600 mb-4\">Plan not found</p>\n <Button as=\"a\" href=\"/catalog/internet\" className=\"flex items-center\">\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Internet Plans\n </Button>\n </div>\n </PageLayout>\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 <PageLayout icon={<></>} title=\"\" description=\"\">\n <div className=\"max-w-4xl mx-auto\">\n <div className=\"text-center mb-12\">\n <Button\n as=\"a\"\n href=\"/catalog/internet\"\n variant=\"outline\"\n size=\"sm\"\n className=\"mb-6 group\"\n >\n <ArrowLeftIcon className=\"w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform\" />\n Back to Internet Plans\n </Button>\n\n <h1 className=\"text-4xl font-bold text-gray-900 mb-4\">Configure {plan.name}</h1>\n\n <div className=\"inline-flex items-center gap-3 bg-gray-50 px-6 py-3 rounded-2xl border\">\n <div\n className={`px-3 py-1 rounded-full text-sm font-medium ${\n plan.tier === \"Platinum\"\n ? \"bg-purple-100 text-purple-800\"\n : plan.tier === \"Gold\"\n ? \"bg-yellow-100 text-yellow-800\"\n : \"bg-gray-100 text-gray-800\"\n }`}\n >\n {plan.tier}\n </div>\n <span className=\"text-gray-600\">•</span>\n <span className=\"font-medium text-gray-900\">{plan.name}</span>\n {plan.monthlyPrice && (\n <>\n <span className=\"text-gray-600\">•</span>\n <span className=\"font-bold text-gray-900\">\n ¥{plan.monthlyPrice.toLocaleString()}/month\n </span>\n </>\n )}\n </div>\n </div>\n\n <ProgressSteps steps={steps} currentStep={currentStep} />\n\n <div className=\"space-y-8\">\n {currentStep === 1 && (\n <AnimatedCard\n variant=\"static\"\n className={`p-8 transition-all duration-500 ease-in-out transform ${isTransitioning ? \"opacity-0 translate-y-4\" : \"opacity-100 translate-y-0\"}`}\n >\n <div className=\"mb-6\">\n <div className=\"flex items-center gap-3 mb-2\">\n <div className=\"w-8 h-8 bg-blue-600 text-white rounded-full flex items-center justify-center text-sm font-semibold\">\n 1\n </div>\n <h3 className=\"text-xl font-semibold text-gray-900\">Service Configuration</h3>\n </div>\n <p className=\"text-gray-600 ml-11\">Review your plan details and configuration</p>\n </div>\n\n {plan?.tier === \"Platinum\" && (\n <div className=\"bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6\">\n <div className=\"flex items-start\">\n <div className=\"flex-shrink-0\">\n <svg\n className=\"w-5 h-5 text-yellow-600 mt-0.5\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </div>\n <div className=\"ml-3\">\n <h5 className=\"font-medium text-yellow-900\">\n IMPORTANT - For PLATINUM subscribers\n </h5>\n <p className=\"text-sm text-yellow-800\">\n Additional fees are incurred for the PLATINUM service. Please refer to the\n information from our tech team for details.\n </p>\n <p className=\"text-xs text-yellow-700 mt-2\">\n * Will appear on the invoice as \"Platinum Base Plan\". Device subscriptions\n will be added later.\n </p>\n </div>\n </div>\n </div>\n )}\n\n {plan?.tier === \"Silver\" ? (\n <div className=\"mb-6\">\n <h4 className=\"font-medium text-gray-900 mb-4\">\n Select Your Router & ISP Configuration:\n </h4>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6\">\n <button\n onClick={() => 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 <h5 className=\"text-lg font-semibold text-gray-900\">PPPoE</h5>\n <p className=\"text-sm text-gray-600 mb-3\">\n Requires a PPPoE-capable router and ISP credentials.\n </p>\n <div className=\"bg-yellow-100 rounded-lg p-3\">\n <p className=\"text-xs text-yellow-800\">\n <strong>Note:</strong> Older standard, may be slower during peak times.\n </p>\n </div>\n </button>\n\n <button\n onClick={() => 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 <h5 className=\"text-lg font-semibold text-gray-900\">IPoE (v6plus)</h5>\n <p className=\"text-sm text-gray-600 mb-3\">\n Requires a v6plus-compatible router for faster, more stable connection.\n </p>\n <div className=\"bg-green-100 rounded-lg p-3\">\n <p className=\"text-xs text-green-800\">\n <strong>Recommended:</strong> Faster speeds with less congestion.{\" \"}\n <a\n href=\"https://www.jpix.ad.jp/service/?p=3565\"\n target=\"_blank\"\n className=\"text-blue-600 underline ml-1\"\n >\n Check compatibility →\n </a>\n </p>\n </div>\n </button>\n </div>\n\n <div className=\"flex justify-end mt-6\">\n <Button\n onClick={() => mode && transitionToStep(2)}\n disabled={!mode}\n className=\"flex items-center\"\n >\n Continue to Installation\n <ArrowRightIcon className=\"w-4 h-4 ml-2\" />\n </Button>\n </div>\n </div>\n ) : (\n <div className=\"mb-6\">\n <div className=\"bg-green-50 border border-green-200 rounded-lg p-4\">\n <div className=\"flex items-center\">\n <svg\n className=\"w-5 h-5 text-green-600 mr-2\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z\"\n clipRule=\"evenodd\"\n />\n </svg>\n <span className=\"font-medium text-green-900\">\n Access Mode: IPoE-HGW (Pre-configured for {plan?.tier} plan)\n </span>\n </div>\n </div>\n <div className=\"flex justify-end mt-6\">\n <Button\n onClick={() => {\n setMode(\"IPoE-BYOR\");\n transitionToStep(2);\n }}\n className=\"flex items-center\"\n >\n Continue to Installation\n <ArrowRightIcon className=\"w-4 h-4 ml-2\" />\n </Button>\n </div>\n </div>\n )}\n </AnimatedCard>\n )}\n\n {currentStep === 2 && mode && (\n <AnimatedCard\n variant=\"static\"\n className={`p-8 transition-all duration-500 ease-in-out transform ${isTransitioning ? \"opacity-0 translate-y-4\" : \"opacity-100 translate-y-0\"}`}\n >\n <div className=\"mb-6\">\n <StepHeader\n stepNumber={2}\n title=\"Installation Options\"\n description=\"Choose your installation payment plan\"\n />\n </div>\n\n <InstallationOptions\n installations={installations}\n selectedInstallation={installations.find(inst => inst.type === installPlan) || null}\n onInstallationSelect={installation => setInstallPlan(installation.type)}\n showSkus={false}\n />\n\n <div className=\"bg-blue-50 border border-blue-200 rounded-lg p-4 mt-6\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-shrink-0\">\n <svg\n className=\"w-5 h-5 text-blue-600 mt-0.5\"\n fill=\"currentColor\"\n viewBox=\"0 0 20 20\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </div>\n <div>\n <h4 className=\"font-medium text-blue-900\">Weekend Installation</h4>\n <p className=\"text-sm text-blue-700 mt-1\">\n Weekend installation is available with an additional ¥3,000 charge. Our team\n will contact you to schedule the most convenient time.\n </p>\n </div>\n </div>\n </div>\n\n <div className=\"flex justify-between mt-6\">\n <Button\n onClick={() => transitionToStep(1)}\n variant=\"outline\"\n className=\"flex items-center\"\n >\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Service Details\n </Button>\n <Button\n onClick={() => installPlan && transitionToStep(3)}\n disabled={!installPlan}\n className=\"flex items-center\"\n >\n Continue to Add-ons\n <ArrowRightIcon className=\"w-4 h-4 ml-2\" />\n </Button>\n </div>\n </AnimatedCard>\n )}\n\n {currentStep === 3 && installPlan && (\n <AnimatedCard\n variant=\"static\"\n className={`p-8 transition-all duration-500 ease-in-out transform ${isTransitioning ? \"opacity-0 translate-y-4\" : \"opacity-100 translate-y-0\"}`}\n >\n <div className=\"mb-6\">\n <StepHeader\n stepNumber={3}\n title=\"Add-ons\"\n description=\"Optional services to enhance your internet experience\"\n />\n </div>\n <AddonGroup\n addons={addons}\n selectedAddonSkus={selectedAddonSkus}\n onAddonToggle={handleAddonSelection}\n showSkus={false}\n />\n <div className=\"flex justify-between mt-6\">\n <Button\n onClick={() => transitionToStep(2)}\n variant=\"outline\"\n className=\"flex items-center\"\n >\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Installation\n </Button>\n <Button onClick={() => transitionToStep(4)} className=\"flex items-center\">\n Review Order\n <ArrowRightIcon className=\"w-4 h-4 ml-2\" />\n </Button>\n </div>\n </AnimatedCard>\n )}\n\n {currentStep === 4 && (\n <AnimatedCard\n variant=\"static\"\n className={`p-8 transition-all duration-500 ease-in-out transform ${isTransitioning ? \"opacity-0 translate-y-4\" : \"opacity-100 translate-y-0\"}`}\n >\n <div className=\"mb-6\">\n <StepHeader\n stepNumber={4}\n title=\"Review Your Order\"\n description=\"Review your configuration and proceed to checkout\"\n />\n </div>\n\n <div className=\"max-w-lg mx-auto mb-8 bg-gradient-to-b from-white to-gray-50 shadow-xl rounded-lg border border-gray-200 p-6\">\n <div className=\"text-center border-b-2 border-dashed border-gray-300 pb-4 mb-6\">\n <h3 className=\"text-xl font-bold text-gray-900 mb-1\">Order Summary</h3>\n <p className=\"text-sm text-gray-500\">Review your configuration</p>\n </div>\n\n <div className=\"space-y-3 mb-6\">\n <div className=\"flex justify-between items-start\">\n <div>\n <h4 className=\"font-semibold text-gray-900\">{plan.name}</h4>\n <p className=\"text-sm text-gray-600\">Internet Service</p>\n </div>\n <div className=\"text-right\">\n <p className=\"font-semibold text-gray-900\">\n ¥{plan.monthlyPrice?.toLocaleString()}\n </p>\n <p className=\"text-xs text-gray-500\">per month</p>\n </div>\n </div>\n </div>\n\n <div className=\"border-t border-gray-200 pt-4 mb-6\">\n <h4 className=\"font-medium text-gray-900 mb-3\">Configuration</h4>\n <div className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between\">\n <span className=\"text-gray-600\">Access Mode:</span>\n <span className=\"text-gray-900\">{mode || \"Not selected\"}</span>\n </div>\n </div>\n </div>\n\n {selectedAddonSkus.length > 0 && (\n <div className=\"border-t border-gray-200 pt-4 mb-6\">\n <h4 className=\"font-medium text-gray-900 mb-3\">Add-ons</h4>\n <div className=\"space-y-2\">\n {selectedAddonSkus.map(addonSku => {\n const addon = addons.find(a => a.sku === addonSku);\n return (\n <div key={addonSku} className=\"flex justify-between text-sm\">\n <span className=\"text-gray-600\">{addon?.name || addonSku}</span>\n <span className=\"text-gray-900\">\n ¥\n {(\n addon?.monthlyPrice ||\n addon?.activationPrice ||\n 0\n ).toLocaleString()}\n <span className=\"text-xs text-gray-500 ml-1\">\n /{addon?.monthlyPrice ? \"mo\" : \"once\"}\n </span>\n </span>\n </div>\n );\n })}\n </div>\n </div>\n )}\n\n {(() => {\n const installation = installations.find(i => i.type === installPlan);\n return installation && installation.price && installation.price > 0 ? (\n <div className=\"border-t border-gray-200 pt-4 mb-6\">\n <h4 className=\"font-medium text-gray-900 mb-3\">Installation</h4>\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-gray-600\">{installation.name}</span>\n <span className=\"text-gray-900\">\n ¥{installation.price.toLocaleString()}\n <span className=\"text-xs text-gray-500 ml-1\">\n /{installation.billingCycle === \"Monthly\" ? \"mo\" : \"once\"}\n </span>\n </span>\n </div>\n </div>\n ) : null;\n })()}\n\n <div className=\"border-t-2 border-dashed border-gray-300 pt-4 bg-gray-50 -mx-6 px-6 py-4 rounded-b-lg\">\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-xl font-bold\">\n <span className=\"text-gray-900\">Monthly Total</span>\n <span className=\"text-blue-600\">¥{monthlyTotal.toLocaleString()}</span>\n </div>\n {oneTimeTotal > 0 && (\n <div className=\"flex justify-between text-sm\">\n <span className=\"text-gray-600\">One-time Total</span>\n <span className=\"text-orange-600 font-semibold\">\n ¥{oneTimeTotal.toLocaleString()}\n </span>\n </div>\n )}\n </div>\n </div>\n </div>\n\n <div className=\"flex justify-between items-center pt-6 border-t\">\n <Button\n onClick={() => transitionToStep(3)}\n variant=\"outline\"\n size=\"lg\"\n className=\"px-8 py-4 text-lg\"\n >\n <ArrowLeftIcon className=\"w-5 h-5 mr-2\" />\n Back to Add-ons\n </Button>\n <Button onClick={onConfirm} size=\"lg\" className=\"px-12 py-4 text-lg font-semibold\">\n Proceed to Checkout\n <ArrowRightIcon className=\"w-5 h-5 ml-2\" />\n </Button>\n </div>\n </AnimatedCard>\n )}\n </div>\n </div>\n </PageLayout>\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 `<div·className=\"text-xs·text-green-600·font-medium·mt-1\">Discounted·price</div>` with `(⏎··········<div·className=\"text-xs·text-green-600·font-medium·mt-1\">Discounted·price</div>⏎········)`","line":31,"column":22,"nodeType":null,"messageId":"replace","endLine":31,"endColumn":101,"fix":{"range":[1460,1539],"text":"(\n <div className=\"text-xs text-green-600 font-medium mt-1\">Discounted price</div>\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 <AnimatedCard variant={isFamily ? \"success\" : \"default\"} className=\"p-6 w-full max-w-sm\">\n <div className=\"flex items-start justify-between mb-3\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <DevicePhoneMobileIcon className=\"h-4 w-4 text-blue-600\" />\n <span className=\"font-bold text-sm text-gray-900\">{plan.dataSize}</span>\n </div>\n {isFamily && (\n <div className=\"flex items-center gap-1 mb-1\">\n <UsersIcon className=\"h-4 w-4 text-green-600\" />\n <span className=\"bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full font-medium\">Family</span>\n </div>\n )}\n </div>\n </div>\n <div className=\"mb-3\">\n <div className=\"flex items-baseline gap-1\">\n <CurrencyYenIcon className=\"h-4 w-4 text-gray-600\" />\n <span className=\"text-xl font-bold text-gray-900\">{plan.monthlyPrice?.toLocaleString()}</span>\n <span className=\"text-gray-600 text-sm\">/month</span>\n </div>\n {isFamily && <div className=\"text-xs text-green-600 font-medium mt-1\">Discounted price</div>}\n </div>\n <div className=\"mb-4\">\n <p className=\"text-xs text-gray-600 line-clamp-2\">{plan.description}</p>\n </div>\n <Button as=\"a\" href={`/catalog/sim/configure?plan=${plan.sku}`} className=\"w-full\" size=\"sm\">\n Configure\n </Button>\n </AnimatedCard>\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 <div className=\"animate-in fade-in duration-500\">\n <div className=\"flex items-center gap-3 mb-6\">\n {icon}\n <div>\n <h2 className=\"text-2xl font-bold text-gray-900\">{title}</h2>\n <p className=\"text-gray-600\">{description}</p>\n </div>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-6 justify-items-center\">\n {regularPlans.map(plan => (\n <SimPlanCard key={plan.id} plan={plan} />\n ))}\n </div>\n {showFamilyDiscount && familyPlans.length > 0 && (\n <>\n <div className=\"flex items-center gap-2 mb-4\">\n <UsersIcon className=\"h-5 w-5 text-green-600\" />\n <h3 className=\"text-lg font-semibold text-green-900\">Family Discount Options</h3>\n <span className=\"bg-green-100 text-green-800 text-xs px-2 py-1 rounded-full\">You qualify!</span>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 justify-items-center\">\n {familyPlans.map(plan => (\n <SimPlanCard key={plan.id} plan={plan} isFamily />\n ))}\n </div>\n </>\n )}\n </div>\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={<WifiIcon·className=\"h-10·w-10·text-blue-600\"·/>}·title=\"Location-Based·Plans\"·description=\"Internet·plans·tailored·to·your·house·type·and·infrastructure\"` with `⏎··············icon={<WifiIcon·className=\"h-10·w-10·text-blue-600\"·/>}⏎··············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={<WifiIcon className=\"h-10 w-10 text-blue-600\" />}\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={<GlobeAltIcon·className=\"h-10·w-10·text-purple-600\"·/>}·title=\"Seamless·Integration\"·description=\"Manage·all·services·from·a·single·account\"` with `⏎··············icon={<GlobeAltIcon·className=\"h-10·w-10·text-purple-600\"·/>}⏎··············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={<GlobeAltIcon className=\"h-10 w-10 text-purple-600\" />}\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 <PageLayout icon={<></>} title=\"\" description=\"\">\n <div className=\"max-w-6xl mx-auto\">\n <div className=\"text-center mb-16\">\n <div className=\"inline-flex items-center gap-2 bg-blue-50 text-blue-700 px-4 py-2 rounded-full text-sm font-medium mb-6\">\n <Squares2X2Icon className=\"h-4 w-4\" />\n Services Catalog\n </div>\n <h1 className=\"text-5xl font-bold text-gray-900 mb-6 leading-tight\">\n Choose Your Perfect\n <br />\n <span className=\"bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent\">Connectivity Solution</span>\n </h1>\n <p className=\"text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed\">\n Discover high-speed internet, mobile data/voice options, and secure VPN services.\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 lg:grid-cols-3 gap-8 mb-16\">\n <ServiceHeroCard\n title=\"Internet Service\"\n description=\"Ultra-high-speed fiber internet with speeds up to 10Gbps.\"\n icon={<ServerIcon className=\"h-12 w-12\" />}\n features={[\"Up to 10Gbps speeds\", \"Fiber optic technology\", \"Multiple access modes\", \"Professional installation\"]}\n href=\"/catalog/internet\"\n color=\"blue\"\n />\n <ServiceHeroCard\n title=\"SIM & eSIM\"\n description=\"Data, SMS, and voice plans with both physical SIM and eSIM options.\"\n icon={<DevicePhoneMobileIcon className=\"h-12 w-12\" />}\n features={[\"Physical SIM & eSIM\", \"Data + SMS/Voice plans\", \"Family discounts\", \"Multiple data options\"]}\n href=\"/catalog/sim\"\n color=\"green\"\n />\n <ServiceHeroCard\n title=\"VPN Service\"\n description=\"Secure remote access solutions for business and personal use.\"\n icon={<ShieldCheckIcon className=\"h-12 w-12\" />}\n features={[\"Secure encryption\", \"Multiple locations\", \"Business & personal\", \"24/7 connectivity\"]}\n href=\"/catalog/vpn\"\n color=\"purple\"\n />\n </div>\n\n <div className=\"bg-gradient-to-br from-gray-50 to-blue-50 rounded-3xl p-10 border border-gray-100\">\n <div className=\"text-center mb-10\">\n <h2 className=\"text-3xl font-bold text-gray-900 mb-4\">Why Choose Our Services?</h2>\n <p className=\"text-lg text-gray-600 max-w-3xl mx-auto leading-relaxed\">\n Personalized recommendations based on your location and account eligibility.\n </p>\n </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-8\">\n <FeatureCard icon={<WifiIcon className=\"h-10 w-10 text-blue-600\" />} title=\"Location-Based Plans\" description=\"Internet plans tailored to your house type and infrastructure\" />\n <FeatureCard icon={<GlobeAltIcon className=\"h-10 w-10 text-purple-600\" />} title=\"Seamless Integration\" description=\"Manage all services from a single account\" />\n </div>\n </div>\n </div>\n </PageLayout>\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":148,"column":24,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[5836,5941],"text":"\n We couldn&apos;t find any internet plans available for your location at this time.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[5836,5941],"text":"\n We couldn&lsquo;t find any internet plans available for your location at this time.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[5836,5941],"text":"\n We couldn&#39;t find any internet plans available for your location at this time.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[5836,5941],"text":"\n We couldn&rsquo;t find any internet plans available for your location at this time.\n "},"desc":"Replace with `&rsquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":229,"column":90,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet&apos;s Hikari\n Next - "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet&lsquo;s Hikari\n Next - "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet&#39;s Hikari\n Next - "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[9005,9065],"text":"1 NTT Optical Fiber (Flet&rsquo;s Hikari\n Next - "},"desc":"Replace with `&rsquo;`."}]}],"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<string>(\"\");\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 <HomeIcon className=\"h-5 w-5\" />;\n if (offeringType.toLowerCase().includes(\"apartment\"))\n return <BuildingOfficeIcon className=\"h-5 w-5\" />;\n return <HomeIcon className=\"h-5 w-5\" />;\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 <PageLayout\n title=\"Internet Plans\"\n description=\"Loading your personalized plans...\"\n icon={<WifiIcon className=\"h-6 w-6\" />}\n >\n <div className=\"flex items-center justify-center py-12\">\n <LoadingSpinner size=\"lg\" label=\"Loading your personalized plans...\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n const errorMessage = error instanceof Error ? error.message : \"An unexpected error occurred\";\n return (\n <PageLayout\n title=\"Internet Plans\"\n description=\"Error loading plans\"\n icon={<WifiIcon className=\"h-6 w-6\" />}\n >\n <div className=\"rounded-lg bg-red-50 border border-red-200 p-6\">\n <div className=\"text-red-800 font-medium\">Failed to load plans</div>\n <div className=\"text-red-600 text-sm mt-1\">{errorMessage}</div>\n <Button as=\"a\" href=\"/catalog\" className=\"flex items-center mt-4\">\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Services\n </Button>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"Internet Plans\"\n description=\"High-speed internet services for your home or business\"\n icon={<WifiIcon className=\"h-6 w-6\" />}\n >\n <div className=\"max-w-6xl mx-auto\">\n <div className=\"mb-6\">\n <Button as=\"a\" href=\"/catalog\" variant=\"outline\" size=\"sm\" className=\"group\">\n <ArrowLeftIcon className=\"w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform duration-300\" />\n Back to Services\n </Button>\n </div>\n\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-gray-900 mb-4\">Choose Your Internet Plan</h1>\n\n {eligibility && (\n <div className=\"mt-6\">\n <div\n className={`inline-flex items-center gap-2 px-6 py-3 rounded-2xl border ${getEligibilityColor(eligibility)}`}\n >\n {getEligibilityIcon(eligibility)}\n <span className=\"font-medium\">Available for: {eligibility}</span>\n </div>\n <p className=\"text-sm text-gray-500 mt-2 max-w-2xl mx-auto\">\n Plans shown are tailored to your house type and local infrastructure\n </p>\n </div>\n )}\n </div>\n\n {plans.length > 0 ? (\n <>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {plans.map(plan => (\n <InternetPlanCard key={plan.id} plan={plan} installations={installations} />\n ))}\n </div>\n\n <div className=\"mt-12 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-6 border border-blue-200\">\n <h4 className=\"font-medium text-blue-900 mb-4 text-lg\">Important Notes:</h4>\n <ul className=\"text-sm text-blue-800 space-y-2\">\n <li className=\"flex items-start\">\n <span className=\"text-blue-600 mr-2\">•</span>Theoretical internet speed is the\n same for all three packages\n </li>\n <li className=\"flex items-start\">\n <span className=\"text-blue-600 mr-2\">•</span>One-time fee (¥22,800) can be paid\n upfront or in 12- or 24-month installments\n </li>\n <li className=\"flex items-start\">\n <span className=\"text-blue-600 mr-2\">•</span>Home phone line (Hikari Denwa) can be\n added to GOLD or PLATINUM plans (¥450/month + ¥1,000-3,000 one-time)\n </li>\n <li className=\"flex items-start\">\n <span className=\"text-blue-600 mr-2\">•</span>In-home technical assistance\n available (¥15,000 onsite visiting fee)\n </li>\n </ul>\n </div>\n </>\n ) : (\n <div className=\"text-center py-12\">\n <ServerIcon className=\"h-12 w-12 text-gray-400 mx-auto mb-4\" />\n <h3 className=\"text-lg font-medium text-gray-900 mb-2\">No Plans Available</h3>\n <p className=\"text-gray-600 mb-6\">\n We couldn't find any internet plans available for your location at this time.\n </p>\n <Button as=\"a\" href=\"/catalog\" className=\"flex items-center\">\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Services\n </Button>\n </div>\n )}\n </div>\n </PageLayout>\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 <AnimatedCard\n variant={cardVariant}\n className={`overflow-hidden flex flex-col h-full ${getBorderClass()}`}\n >\n <div className=\"p-6 flex flex-col flex-grow\">\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n <span\n className={`px-3 py-1 rounded-full text-sm font-medium border ${isGold ? \"bg-yellow-100 text-yellow-800 border-yellow-300\" : isPlatinum ? \"bg-purple-100 text-purple-800 border-purple-300\" : \"bg-gray-100 text-gray-800 border-gray-300\"}`}\n >\n {plan.tier}\n </span>\n {isGold && (\n <span className=\"px-2 py-1 bg-green-100 text-green-800 text-xs font-medium rounded-full\">\n Recommended\n </span>\n )}\n </div>\n {plan.monthlyPrice && (\n <div className=\"text-right\">\n <div className=\"flex items-baseline justify-end gap-1 text-2xl font-bold text-gray-900\">\n <CurrencyYenIcon className=\"h-6 w-6\" />\n <span>{plan.monthlyPrice.toLocaleString()}</span>\n <span className=\"text-sm text-gray-500 font-normal whitespace-nowrap\">\n per month\n </span>\n </div>\n </div>\n )}\n </div>\n\n <h3 className=\"text-xl font-semibold text-gray-900 mb-2\">{plan.name}</h3>\n <p className=\"text-gray-600 text-sm mb-4\">{plan.tierDescription || plan.description}</p>\n\n <div className=\"mb-6 flex-grow\">\n <h4 className=\"font-medium text-gray-900 mb-3\">Your Plan Includes:</h4>\n <ul className=\"space-y-2 text-sm text-gray-700\">\n {plan.features && plan.features.length > 0 ? (\n plan.features.map((feature, index) => (\n <li key={index} className=\"flex items-start\">\n <span className=\"text-green-600 mr-2\">✓</span>\n {feature}\n </li>\n ))\n ) : (\n <>\n <li className=\"flex items-start\">\n <span className=\"text-green-600 mr-2\">✓</span>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 </li>\n <li className=\"flex items-start\">\n <span className=\"text-green-600 mr-2\">✓</span>\n Monthly: ¥{plan.monthlyPrice?.toLocaleString()}\n {installations.length > 0 && (\n <span className=\"text-gray-600 text-sm ml-2\">\n (+ installation from ¥\n {Math.min(...installations.map(i => i.price || 0)).toLocaleString()})\n </span>\n )}\n </li>\n </>\n )}\n </ul>\n </div>\n\n <Button\n as=\"a\"\n href={`/catalog/internet/configure?plan=${plan.sku}`}\n className=\"w-full group\"\n >\n <span>Configure Plan</span>\n <ArrowRightIcon className=\"w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform duration-300\" />\n </Button>\n </div>\n </AnimatedCard>\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":122,"column":24,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[4876,4958],"text":"\n We couldn&apos;t find any VPN plans available at this time.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[4876,4958],"text":"\n We couldn&lsquo;t find any VPN plans available at this time.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[4876,4958],"text":"\n We couldn&#39;t find any VPN plans available at this time.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[4876,4958],"text":"\n We couldn&rsquo;t find any VPN plans available at this time.\n "},"desc":"Replace with `&rsquo;`."}]}],"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 <PageLayout\n title=\"VPN Plans\"\n description=\"Loading plans...\"\n icon={<ShieldCheckIcon className=\"h-6 w-6\" />}\n >\n <div className=\"flex items-center justify-center py-12\">\n <LoadingSpinner size=\"lg\" label=\"Loading VPN plans...\" />\n </div>\n </PageLayout>\n );\n }\n\n if (error) {\n const errorMessage = error instanceof Error ? error.message : \"An unexpected error occurred\";\n return (\n <PageLayout\n title=\"VPN Plans\"\n description=\"Error loading plans\"\n icon={<ShieldCheckIcon className=\"h-6 w-6\" />}\n >\n <div className=\"rounded-lg bg-red-50 border border-red-200 p-6\">\n <div className=\"text-red-800 font-medium\">Failed to load VPN plans</div>\n <div className=\"text-red-600 text-sm mt-1\">{errorMessage}</div>\n <Button as=\"a\" href=\"/catalog\" className=\"flex items-center mt-4\">\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Services\n </Button>\n </div>\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title=\"VPN Router Rental\"\n description=\"Secure VPN router rental\"\n icon={<ShieldCheckIcon className=\"h-6 w-6\" />}\n >\n <div className=\"max-w-6xl mx-auto\">\n <div className=\"mb-6\">\n <Button as=\"a\" href=\"/catalog\" variant=\"outline\" size=\"sm\" className=\"group\">\n <ArrowLeftIcon className=\"w-5 h-5 mr-2 group-hover:-translate-x-1 transition-transform duration-300\" />\n Back to Services\n </Button>\n </div>\n\n <div className=\"text-center mb-12\">\n <h1 className=\"text-4xl font-bold text-gray-900 mb-4\">\n SonixNet VPN Rental Router Service\n </h1>\n <p className=\"text-xl text-gray-600 max-w-3xl mx-auto\">\n Fast and secure VPN connection to San Francisco or London for accessing geo-restricted\n content.\n </p>\n </div>\n\n {vpnPlans.length > 0 ? (\n <div className=\"mb-8\">\n <h2 className=\"text-2xl font-bold text-gray-900 mb-2 text-center\">Available Plans</h2>\n <p className=\"text-gray-600 text-center mb-6\">(One region per router)</p>\n\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto\">\n {vpnPlans.map(plan => (\n <AnimatedCard\n key={plan.id}\n className=\"p-6 border-2 border-blue-200 hover:border-blue-300 transition-colors\"\n >\n <div className=\"text-center mb-4\">\n <h3 className=\"text-xl font-bold text-gray-900\">{plan.name}</h3>\n </div>\n <div className=\"mb-4 text-center\">\n <div className=\"flex items-baseline justify-center gap-1\">\n <CurrencyYenIcon className=\"h-5 w-5 text-gray-600\" />\n <span className=\"text-3xl font-bold text-gray-900\">\n {plan.monthlyPrice?.toLocaleString()}\n </span>\n <span className=\"text-gray-600\">/month</span>\n </div>\n </div>\n <Button\n as=\"a\"\n href={`/catalog/vpn/configure?plan=${plan.sku}`}\n className=\"w-full\"\n >\n Configure Plan\n </Button>\n </AnimatedCard>\n ))}\n </div>\n\n {activationFees.length > 0 && (\n <div className=\"mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg max-w-4xl mx-auto\">\n <p className=\"text-sm text-blue-800 text-center\">\n A one-time activation fee of 3000 JPY is incurred seprarately for each rental\n unit. Tax (10%) not included.\n </p>\n </div>\n )}\n </div>\n ) : (\n <div className=\"text-center py-12\">\n <ShieldCheckIcon className=\"h-12 w-12 text-gray-400 mx-auto mb-4\" />\n <h3 className=\"text-lg font-medium text-gray-900 mb-2\">No VPN Plans Available</h3>\n <p className=\"text-gray-600 mb-6\">\n We couldn't find any VPN plans available at this time.\n </p>\n <Button as=\"a\" href=\"/catalog\" className=\"flex items-center\">\n <ArrowLeftIcon className=\"w-4 h-4 mr-2\" />\n Back to Services\n </Button>\n </div>\n )}\n\n <div className=\"bg-white rounded-xl border border-gray-200 p-8 mb-8\">\n <h2 className=\"text-2xl font-bold text-gray-900 mb-6\">How It Works</h2>\n <div className=\"space-y-4 text-gray-700\">\n <p>\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 </p>\n <p>\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 </p>\n <p>\n Then you can connect your network media players to the VPN Wi-Fi network, to connect\n to the VPN server.\n </p>\n <p>\n For daily Internet usage that does not require a VPN, we recommend connecting to your\n regular home Wi-Fi.\n </p>\n </div>\n </div>\n\n <div className=\"bg-yellow-50 border border-yellow-200 rounded-xl p-6 mb-8\">\n <h3 className=\"font-bold text-yellow-900 mb-3\">Important Disclaimer</h3>\n <p className=\"text-sm text-yellow-800\">\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 </p>\n </div>\n </div>\n </PageLayout>\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<Address | null>(null);\n\n const [checkoutState, setCheckoutState] = useState<CheckoutState>({\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<string, string> = {};\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<string, unknown> = {};\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<React.SVGProps<SVGSVGElement>>\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 <Wrapper\n className={cn(\n \"flex items-start space-x-4 w-full text-left\",\n onClick && [\n \"p-3 -m-3 rounded-lg transition-all duration-200 cursor-pointer group\",\n \"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2\",\n \"active:scale-[0.98]\",\n ],\n className\n )}\n onClick={onClick}\n type={onClick ? \"button\" : undefined}\n >\n <div className=\"flex-shrink-0\">\n <div\n className={cn(\n \"w-10 h-10 rounded-full bg-gradient-to-r flex items-center justify-center shadow-md\",\n \"transition-transform duration-200\",\n onClick && \"group-hover:scale-110\",\n gradient\n )}\n >\n <Icon className=\"h-5 w-5 text-white\" />\n </div>\n </div>\n\n <div className=\"flex-1 min-w-0\">\n <p\n className={cn(\n \"text-sm font-medium transition-colors duration-200 truncate\",\n onClick ? \"text-blue-900 group-hover:text-blue-700\" : \"text-gray-900\"\n )}\n >\n {title}\n </p>\n\n {description && <p className=\"text-sm text-gray-500 mt-1 line-clamp-2\">{description}</p>}\n\n <p className=\"text-xs text-gray-400 mt-2 flex items-center\">\n <time dateTime={date} title={format(new Date(date), \"MMMM d, yyyy 'at' h:mm a\")}>\n {formattedDate}\n </time>\n </p>\n </div>\n\n {onClick && (\n <div className=\"flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200\">\n <svg\n className=\"h-4 w-4 text-gray-400 group-hover:text-blue-600\"\n viewBox=\"0 0 20 20\"\n fill=\"currentColor\"\n >\n <path\n fillRule=\"evenodd\"\n d=\"M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z\"\n clipRule=\"evenodd\"\n />\n </svg>\n </div>\n )}\n </Wrapper>\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<DashboardSummary, DashboardError>({\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<DashboardUIState[\"preferences\"]>) => 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<DashboardUIState>()(\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":191,"column":22,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it&apos;s approved and ready for activation.\n "},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it&lsquo;s approved and ready for activation.\n "},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it&#39;s approved and ready for activation.\n "},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[5645,5820],"text":"\n Your order has been created and submitted for processing. We will notify you as soon\n as it&rsquo;s approved and ready for activation.\n "},"desc":"Replace with `&rsquo;`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":199,"column":26,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[6167,6217],"text":"You&apos;ll receive an email confirmation once approved"},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[6167,6217],"text":"You&lsquo;ll receive an email confirmation once approved"},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[6167,6217],"text":"You&#39;ll receive an email confirmation once approved"},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[6167,6217],"text":"You&rsquo;ll receive an email confirmation once approved"},"desc":"Replace with `&rsquo;`."}]}],"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 <WifiIcon className=\"h-6 w-6\" />;\n case \"SIM\":\n return <DevicePhoneMobileIcon className=\"h-6 w-6\" />;\n case \"VPN\":\n return <LockClosedIcon className=\"h-6 w-6\" />;\n default:\n return <CubeIcon className=\"h-6 w-6\" />;\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<OrderSummary | null>(null);\n const [error, setError] = useState<string | null>(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<OrderSummary>(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 <PageLayout\n icon={<ClipboardDocumentCheckIcon />}\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 && <div className=\"text-red-600 text-sm mb-4\">{error}</div>}\n {isNewOrder && (\n <div className=\"bg-green-50 border border-green-200 rounded-xl p-4 sm:p-6 mb-6\">\n <div className=\"flex items-start\">\n <CheckCircleIcon className=\"h-6 w-6 text-green-600 mt-0.5 mr-3 flex-shrink-0\" />\n <div>\n <h3 className=\"text-lg font-semibold text-green-900 mb-2\">\n Order Submitted Successfully!\n </h3>\n <p className=\"text-green-800 mb-3\">\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 </p>\n <div className=\"text-sm text-green-700\">\n <p className=\"mb-1\">\n <strong>What happens next:</strong>\n </p>\n <ul className=\"list-disc list-inside space-y-1 ml-4\">\n <li>Our team will review your order (within 1 business day)</li>\n <li>You'll receive an email confirmation once approved</li>\n <li>We will schedule activation based on your preferences</li>\n <li>This page will update automatically as your order progresses</li>\n </ul>\n </div>\n </div>\n </div>\n </div>\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 <SubCard\n className=\"mb-9\"\n header={<h3 className=\"text-xl font-bold text-gray-900\">Status</h3>}\n >\n <div className=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 mb-6\">\n <div className=\"text-gray-700 text-lg sm:text-xl\">{statusInfo.description}</div>\n <StatusPill\n label={statusInfo.label}\n variant={statusVariant as \"info\" | \"success\" | \"warning\" | \"error\"}\n />\n </div>\n {statusInfo.nextAction && (\n <div className=\"bg-blue-50 border-2 border-blue-200 rounded-xl p-4 mb-4 shadow-sm\">\n <div className=\"flex items-center gap-2 mb-2\">\n <svg className=\"w-5 h-5 text-blue-600\" fill=\"currentColor\" viewBox=\"0 0 20 20\">\n <path\n fillRule=\"evenodd\"\n d=\"M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z\"\n clipRule=\"evenodd\"\n />\n </svg>\n <span className=\"font-medium text-blue-900\">Next Steps</span>\n </div>\n <p className=\"text-sm text-blue-800\">{statusInfo.nextAction}</p>\n </div>\n )}\n {statusInfo.timeline && (\n <div className=\"text-sm text-gray-500\">{statusInfo.timeline}</div>\n )}\n </SubCard>\n );\n })()}\n {data && (\n <SubCard\n header={\n <div className=\"flex items-center gap-2\">\n {getOrderTypeIcon(data.orderType)}\n <h3 className=\"text-xl font-bold text-gray-900\">Order Items</h3>\n </div>\n }\n >\n {!data.items || data.items.length === 0 ? (\n <div className=\"text-gray-600\">No items on this order.</div>\n ) : (\n <div className=\"space-y-4\">\n {data.items.map(item => (\n <div\n key={item.id}\n className=\"flex items-center justify-between border rounded-lg p-3\"\n >\n <div>\n <div className=\"font-medium text-gray-900\">{item.product.name}</div>\n <div className=\"text-xs text-gray-500\">SKU: {item.product.sku}</div>\n <div className=\"text-xs text-gray-500\">{item.product.billingCycle}</div>\n </div>\n <div className=\"text-right\">\n <div className=\"text-sm text-gray-600\">Qty: {item.quantity}</div>\n <div className=\"text-sm font-semibold text-gray-900\">\n ¥{(item.totalPrice || 0).toLocaleString()}\n </div>\n </div>\n </div>\n ))}\n {(() => {\n const totals = calculateDetailedTotals(data.items || []);\n return (\n <div className=\"flex items-center justify-end\">\n <div className=\"text-right\">\n <div className=\"text-xl font-bold text-gray-900\">\n ¥{totals.monthlyTotal.toLocaleString()}{\" \"}\n <span className=\"text-sm text-gray-500\">/mo</span>\n </div>\n {totals.oneTimeTotal > 0 && (\n <div className=\"text-sm text-orange-600 font-semibold\">\n ¥{totals.oneTimeTotal.toLocaleString()}{\" \"}\n <span className=\"text-xs text-gray-500\">one-time</span>\n </div>\n )}\n </div>\n </div>\n );\n })()}\n </div>\n )}\n </SubCard>\n )}\n </PageLayout>\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 `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`.","line":192,"column":54,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"&apos;"},"fix":{"range":[6072,6106],"text":"You haven&apos;t placed any orders yet."},"desc":"Replace with `&apos;`."},{"messageId":"replaceWithAlt","data":{"alt":"&lsquo;"},"fix":{"range":[6072,6106],"text":"You haven&lsquo;t placed any orders yet."},"desc":"Replace with `&lsquo;`."},{"messageId":"replaceWithAlt","data":{"alt":"&#39;"},"fix":{"range":[6072,6106],"text":"You haven&#39;t placed any orders yet."},"desc":"Replace with `&#39;`."},{"messageId":"replaceWithAlt","data":{"alt":"&rsquo;"},"fix":{"range":[6072,6106],"text":"You haven&rsquo;t placed any orders yet."},"desc":"Replace with `&rsquo;`."}]}],"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 <div className=\"bg-green-50 border border-green-200 rounded-xl p-6 mb-6\">\n <div className=\"flex items-start\">\n <CheckCircleIcon className=\"h-6 w-6 text-green-600 mt-0.5 mr-3 flex-shrink-0\" />\n <div>\n <h3 className=\"text-lg font-semibold text-green-900 mb-2\">\n Order Submitted Successfully!\n </h3>\n <p className=\"text-green-800\">\n Your order has been created and is now being processed. You can track its progress\n below.\n </p>\n </div>\n </div>\n </div>\n );\n}\n\nexport function OrdersListContainer() {\n const router = useRouter();\n const [orders, setOrders] = useState<OrderSummary[]>([]);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchOrders = async () => {\n try {\n const list = await ordersService.getMyOrders<OrderSummary>();\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: <WifiIcon className=\"h-6 w-6\" />, label: \"Internet Service\" };\n case \"SIM\":\n return { icon: <DevicePhoneMobileIcon className=\"h-6 w-6\" />, label: \"Mobile Service\" };\n case \"VPN\":\n return { icon: <LockClosedIcon className=\"h-6 w-6\" />, label: \"VPN Service\" };\n default:\n return { icon: <CubeIcon className=\"h-6 w-6\" />, 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 <PageLayout\n icon={<ClipboardDocumentListIcon />}\n title=\"My Orders\"\n description=\"View and track all your orders\"\n >\n <Suspense fallback={null}>\n <OrdersSuccessBanner />\n </Suspense>\n\n {error && (\n <div className=\"bg-red-50 border border-red-200 rounded-xl p-4 mb-6\">\n <p className=\"text-red-800\">{error}</p>\n </div>\n )}\n\n {loading ? (\n <div className=\"flex items-center justify-center h-40\">\n <div className=\"text-center space-y-3\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"text-gray-600\">Loading your orders...</p>\n </div>\n </div>\n ) : orders.length === 0 ? (\n <AnimatedCard className=\"p-8 text-center\">\n <ClipboardDocumentListIcon className=\"h-12 w-12 text-gray-400 mx-auto mb-4\" />\n <h3 className=\"text-lg font-medium text-gray-900 mb-2\">No orders yet</h3>\n <p className=\"text-gray-600 mb-4\">You haven't placed any orders yet.</p>\n <button\n onClick={() => 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 </button>\n </AnimatedCard>\n ) : (\n <div className=\"space-y-6\">\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 <AnimatedCard\n key={String(order.id)}\n className=\"rounded-2xl p-6 hover:shadow-lg transition-all duration-200 cursor-pointer group\"\n onClick={() => router.push(`/orders/${order.id}`)}\n >\n <div className=\"flex justify-between items-start mb-4\">\n <div className=\"flex items-start gap-4\">\n <div className=\"text-2xl\">{serviceType.icon}</div>\n <div>\n <h3 className=\"text-xl font-bold text-gray-900 group-hover:text-blue-600 transition-colors\">\n {serviceType.label}\n </h3>\n <p className=\"text-sm text-gray-500 mt-1\">\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 </p>\n </div>\n </div>\n <div className=\"text-right\">\n <StatusPill\n label={statusInfo.label}\n variant={\n statusInfo.label === \"Active\"\n ? \"success\"\n : statusInfo.label === \"Setting Up\" || statusInfo.label === \"Under Review\"\n ? \"info\"\n : \"neutral\"\n }\n />\n </div>\n </div>\n <div className=\"bg-gray-50 rounded-xl p-4 mb-4\">\n <div className=\"flex justify-between items-center\">\n <div>\n <p className=\"font-medium text-gray-900\">{serviceSummary}</p>\n <p className=\"text-sm text-gray-600 mt-1\">{statusInfo.description}</p>\n {statusInfo.nextAction && (\n <p className=\"text-sm text-blue-600 mt-1 font-medium\">\n {statusInfo.nextAction}\n </p>\n )}\n </div>\n {(() => {\n const totals = calculateOrderTotals(order);\n if (totals.monthlyTotal <= 0 && totals.oneTimeTotal <= 0) return null;\n return (\n <div className=\"text-right\">\n <div className=\"space-y-1\">\n <p className=\"text-2xl font-bold text-gray-900\">\n ¥{totals.monthlyTotal.toLocaleString()}\n </p>\n <p className=\"text-sm text-gray-500\">per month</p>\n {totals.oneTimeTotal > 0 && (\n <>\n <p className=\"text-lg font-semibold text-orange-600\">\n ¥{totals.oneTimeTotal.toLocaleString()}\n </p>\n <p className=\"text-xs text-gray-500\">one-time</p>\n </>\n )}\n </div>\n <div className=\"mt-3 text-xs text-gray-500 text-left\">\n <p>* Additional fees may apply</p>\n <p className=\"text-gray-400\">(e.g., weekend installation)</p>\n </div>\n </div>\n );\n })()}\n </div>\n </div>\n <div className=\"flex items-center justify-between text-sm\">\n <span className=\"text-gray-500\">Click to view details</span>\n <svg\n className=\"w-5 h-5 text-gray-400 group-hover:text-blue-500 transition-colors\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M9 5l7 7-7 7\"\n />\n </svg>\n </div>\n </AnimatedCard>\n );\n })}\n </div>\n )}\n </PageLayout>\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<T = any>(): Promise<T[]> {\n const res = await apiClient.get<T[]>(\"/orders/user\");\n return (res.data as T[]) || [];\n }\n\n async getOrderById<T = any>(id: string): Promise<T> {\n const res = await apiClient.get<T>(`/orders/${id}`);\n return res.data as T;\n }\n\n async createOrder<T = { sfOrderId: string }>(orderData: unknown): Promise<T> {\n const res = await apiClient.post<T>(\"/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<HTMLDivElement, SimActionsProps>(\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<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [success, setSuccess] = useState<string | null>(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 <div className=\"flex items-center mb-6\">\n <div className=\"bg-blue-50 rounded-xl p-2 mr-4\">\n <Cog6ToothIcon className=\"h-6 w-6 text-blue-600\" />\n </div>\n <div>\n <h3 className=\"text-xl font-semibold text-gray-900\">SIM Management Actions</h3>\n <p className=\"text-sm text-gray-600 mt-1\">Manage your SIM service</p>\n </div>\n </div>\n )}\n {/* Status Messages */}\n {success && (\n <div className=\"mb-4 bg-green-50 border border-green-200 rounded-lg p-4\">\n <div className=\"flex items-center\">\n <CheckCircleIcon className=\"h-5 w-5 text-green-500 mr-2\" />\n <p className=\"text-sm text-green-800\">{success}</p>\n </div>\n </div>\n )}\n\n {error && (\n <div className=\"mb-4 bg-red-50 border border-red-200 rounded-lg p-4\">\n <div className=\"flex items-center\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-red-500 mr-2\" />\n <p className=\"text-sm text-red-800\">{error}</p>\n </div>\n </div>\n )}\n\n {!isActive && (\n <div className=\"mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4\">\n <div className=\"flex items-center\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-yellow-500 mr-2\" />\n <p className=\"text-sm text-yellow-800\">\n SIM management actions are only available for active services.\n </p>\n </div>\n </div>\n )}\n\n {/* Action Buttons */}\n <div className=\"space-y-4\">\n {/* Top Up Data - Primary Action */}\n <div className=\"flex items-center space-x-3 p-4 border border-blue-200 rounded-lg bg-blue-50\">\n <div className=\"flex-shrink-0\">\n <div className=\"bg-blue-100 rounded-lg p-2\">\n <PlusIcon className=\"h-5 w-5 text-blue-600\" />\n </div>\n </div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-semibold text-blue-900\">Top Up Data</h4>\n <p className=\"text-sm text-blue-700 mt-1\">\n Add additional data quota to your SIM service\n </p>\n </div>\n <div className=\"flex-shrink-0\">\n <Button\n variant=\"default\"\n size=\"sm\"\n disabled={!canTopUp || loading !== null}\n loading={loading === \"topup\"}\n loadingText=\"Processing...\"\n onClick={() => {\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 </Button>\n </div>\n </div>\n\n {/* Reissue eSIM (only for eSIMs) */}\n {simType === \"esim\" && (\n <div className=\"flex items-center space-x-3 p-4 border border-green-200 rounded-lg bg-green-50\">\n <div className=\"flex-shrink-0\">\n <div className=\"bg-green-100 rounded-lg p-2\">\n <ArrowPathIcon className=\"h-5 w-5 text-green-600\" />\n </div>\n </div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-semibold text-green-900\">Reissue eSIM</h4>\n <p className=\"text-sm text-green-700 mt-1\">\n Generate a new eSIM profile for download\n </p>\n </div>\n <div className=\"flex-shrink-0\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={!canReissue || loading !== null}\n loading={loading === \"reissue\"}\n loadingText=\"Processing...\"\n onClick={() => {\n setActiveInfo(\"reissue\");\n try {\n router.push(`/subscriptions/${subscriptionId}/sim/reissue`);\n } catch {\n setShowReissueConfirm(true);\n }\n }}\n >\n Reissue\n </Button>\n </div>\n </div>\n )}\n\n {/* Change Plan - Secondary Action */}\n <div className=\"flex items-center space-x-3 p-4 border border-purple-200 rounded-lg bg-purple-50\">\n <div className=\"flex-shrink-0\">\n <div className=\"bg-purple-100 rounded-lg p-2\">\n <svg\n className=\"h-5 w-5 text-purple-600\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4\"\n />\n </svg>\n </div>\n </div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-semibold text-purple-900\">Change Plan</h4>\n <p className=\"text-sm text-purple-700 mt-1\">Switch to a different data plan</p>\n </div>\n <div className=\"flex-shrink-0\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={loading !== null}\n onClick={() => {\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 </Button>\n </div>\n </div>\n\n {/* Cancel SIM - Destructive Action */}\n <div className=\"flex items-center space-x-3 p-4 border border-red-200 rounded-lg bg-red-50\">\n <div className=\"flex-shrink-0\">\n <div className=\"bg-red-100 rounded-lg p-2\">\n <XMarkIcon className=\"h-5 w-5 text-red-600\" />\n </div>\n </div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-semibold text-red-900\">Cancel SIM</h4>\n <p className=\"text-sm text-red-700 mt-1\">Permanently cancel your SIM service</p>\n </div>\n <div className=\"flex-shrink-0\">\n <Button\n variant=\"destructive\"\n size=\"sm\"\n disabled={!canCancel || loading !== null}\n loading={loading === \"cancel\"}\n loadingText=\"Processing...\"\n onClick={() => {\n setActiveInfo(\"cancel\");\n try {\n router.push(`/subscriptions/${subscriptionId}/sim/cancel`);\n } catch {\n setShowCancelConfirm(true);\n }\n }}\n >\n Cancel SIM\n </Button>\n </div>\n </div>\n </div>\n </>\n );\n\n if (embedded) {\n return (\n <div ref={ref} id=\"sim-actions\" className={cn(\"\", className)}>\n {content}\n {/* Modals and confirmations */}\n {renderModals()}\n </div>\n );\n }\n\n return (\n <SubCard\n ref={ref}\n id=\"sim-actions\"\n title=\"SIM Management Actions\"\n icon={<Cog6ToothIcon className=\"h-5 w-5\" />}\n className={cn(\"\", className)}\n >\n {content}\n {/* Modals and confirmations */}\n {renderModals()}\n </SubCard>\n );\n\n function renderModals() {\n return (\n <>\n {/* Top Up Modal */}\n {showTopUpModal && (\n <TopUpModal\n subscriptionId={subscriptionId}\n onClose={() => {\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 <ChangePlanModal\n subscriptionId={subscriptionId}\n currentPlanCode={currentPlanCode}\n onClose={() => {\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 <div className=\"fixed inset-0 z-50 overflow-y-auto\">\n <div className=\"flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0\">\n <div className=\"fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity\"></div>\n <div className=\"inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full\">\n <div className=\"bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4\">\n <div className=\"sm:flex sm:items-start\">\n <div className=\"mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-green-100 sm:mx-0 sm:h-10 sm:w-10\">\n <ArrowPathIcon className=\"h-6 w-6 text-green-600\" />\n </div>\n <div className=\"mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left\">\n <h3 className=\"text-lg leading-6 font-medium text-gray-900\">\n Reissue eSIM Profile\n </h3>\n <div className=\"mt-2\">\n <p className=\"text-sm text-gray-500\">\n This will generate a new eSIM profile for download. Your current eSIM\n will remain active until you activate the new profile.\n </p>\n </div>\n </div>\n </div>\n </div>\n <div className=\"bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse\">\n <button\n type=\"button\"\n onClick={() => 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 </button>\n <button\n type=\"button\"\n onClick={() => {\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 </button>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* Cancel Confirmation */}\n {showCancelConfirm && (\n <div className=\"fixed inset-0 z-50 overflow-y-auto\">\n <div className=\"flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0\">\n <div className=\"fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity\"></div>\n <div className=\"inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full\">\n <div className=\"bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4\">\n <div className=\"sm:flex sm:items-start\">\n <div className=\"mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10\">\n <ExclamationTriangleIcon className=\"h-6 w-6 text-red-600\" />\n </div>\n <div className=\"mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left\">\n <h3 className=\"text-lg leading-6 font-medium text-gray-900\">\n Cancel SIM Service\n </h3>\n <div className=\"mt-2\">\n <p className=\"text-sm text-gray-500\">\n Are you sure you want to cancel this SIM service? This action cannot be\n undone and will permanently terminate your service.\n </p>\n </div>\n </div>\n </div>\n </div>\n <div className=\"bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse\">\n <button\n type=\"button\"\n onClick={() => 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 </button>\n <button\n type=\"button\"\n onClick={() => {\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 </button>\n </div>\n </div>\n </div>\n </div>\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<SimInfo | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<string | null>(null);\n\n const fetchSimInfo = useCallback(async () => {\n try {\n setError(null);\n\n const info = await simActionsService.getSimInfo<SimDetails, SimUsage>(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 <div className=\"space-y-8\">\n <SubCard title=\"SIM Management\" icon={<DevicePhoneMobileIcon className=\"h-5 w-5\" />}>\n <div className=\"flex items-center justify-center py-12\">\n <div className=\"text-center\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"text-gray-600 mt-4\">Loading your SIM service details...</p>\n </div>\n </div>\n </SubCard>\n </div>\n );\n }\n\n if (error) {\n return (\n <SubCard title=\"SIM Management\" icon={<DevicePhoneMobileIcon className=\"h-5 w-5\" />}>\n <ErrorState\n title=\"Unable to Load SIM Information\"\n message={error}\n onRetry={handleRefresh}\n variant=\"page\"\n />\n </SubCard>\n );\n }\n\n if (!simInfo) {\n return null;\n }\n\n return (\n <div id=\"sim-management\" className=\"space-y-8\">\n {/* SIM Details and Usage - Main Content */}\n <div className=\"grid grid-cols-1 xl:grid-cols-3 gap-8\">\n {/* Main Content Area - Actions and Settings (Left Side) */}\n <div className=\"order-2 xl:col-span-2 xl:order-1\">\n <SubCard>\n <SimActions\n subscriptionId={subscriptionId}\n simType={simInfo.details.simType}\n status={simInfo.details.status}\n currentPlanCode={simInfo.details.planCode}\n onTopUpSuccess={handleActionSuccess}\n onPlanChangeSuccess={handleActionSuccess}\n onCancelSuccess={handleActionSuccess}\n onReissueSuccess={handleActionSuccess}\n embedded={true}\n />\n <div className=\"mt-6\">\n <p className=\"text-sm text-gray-600 font-medium mb-3\">Modify service options</p>\n <SimFeatureToggles\n subscriptionId={subscriptionId}\n voiceMailEnabled={simInfo.details.voiceMailEnabled}\n callWaitingEnabled={simInfo.details.callWaitingEnabled}\n internationalRoamingEnabled={simInfo.details.internationalRoamingEnabled}\n networkType={simInfo.details.networkType}\n onChanged={handleActionSuccess}\n embedded\n />\n </div>\n </SubCard>\n </div>\n\n {/* Sidebar - Compact Info (Right Side) */}\n <div className=\"order-1 xl:order-2 space-y-8\">\n {/* Details + Usage combined card for mobile-first */}\n <SubCard>\n <div className=\"space-y-6\">\n <SimDetailsCard\n simDetails={simInfo.details}\n isLoading={false}\n error={null}\n embedded={true}\n showFeaturesSummary={false}\n />\n <DataUsageChart\n usage={simInfo.usage}\n remainingQuotaMb={simInfo.details.remainingQuotaMb}\n isLoading={false}\n error={null}\n embedded={true}\n />\n </div>\n </SubCard>\n\n {/* Important Information Card */}\n <div className=\"bg-gradient-to-br from-blue-50 to-blue-100 border border-blue-200 rounded-xl p-6\">\n <div className=\"flex items-center mb-4\">\n <div className=\"bg-blue-200 rounded-lg p-2 mr-3\">\n <svg\n className=\"h-5 w-5 text-blue-600\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"\n />\n </svg>\n </div>\n <h3 className=\"text-lg font-semibold text-blue-900\">Important Information</h3>\n </div>\n <ul className=\"space-y-2 text-sm text-blue-800\">\n <li className=\"flex items-start\">\n <span className=\"inline-block w-1.5 h-1.5 bg-blue-600 rounded-full mt-2 mr-3 flex-shrink-0\"></span>\n Data usage is updated in real-time and may take a few minutes to reflect recent\n activity\n </li>\n <li className=\"flex items-start\">\n <span className=\"inline-block w-1.5 h-1.5 bg-blue-600 rounded-full mt-2 mr-3 flex-shrink-0\"></span>\n Top-up data will be available immediately after successful processing\n </li>\n <li className=\"flex items-start\">\n <span className=\"inline-block w-1.5 h-1.5 bg-blue-600 rounded-full mt-2 mr-3 flex-shrink-0\"></span>\n SIM cancellation is permanent and cannot be undone\n </li>\n {simInfo.details.simType === \"esim\" && (\n <li className=\"flex items-start\">\n <span className=\"inline-block w-1.5 h-1.5 bg-blue-600 rounded-full mt-2 mr-3 flex-shrink-0\"></span>\n eSIM profile reissue will provide a new QR code for activation\n </li>\n )}\n </ul>\n </div>\n\n {/* (On desktop, details+usage are above; on mobile they appear first since this section is above actions) */}\n </div>\n </div>\n </div>\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 <div className=\"flex items-start space-x-3 p-4 border border-gray-200 rounded-lg hover:border-gray-300 transition-colors\">\n <div className=\"flex-shrink-0 mt-1\">{icon}</div>\n <div className=\"flex-1 min-w-0\">\n <h4 className=\"text-sm font-semibold text-gray-900\">{label}</h4>\n <p className=\"text-sm text-gray-600 mt-1\">{description}</p>\n </div>\n <div className=\"flex-shrink-0\">\n <Button variant={variant} size=\"sm\" disabled={disabled} onClick={onClick}>\n {label}\n </Button>\n </div>\n </div>\n);\n\nexport function SubscriptionActions({\n subscription,\n onActionSuccess,\n className,\n}: SubscriptionActionsProps) {\n const router = useRouter();\n const [loading, setLoading] = useState<string | null>(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 <SubCard\n title=\"Subscription Actions\"\n icon={<Cog6ToothIcon className=\"h-5 w-5\" />}\n className={cn(\"\", className)}\n >\n <div className=\"space-y-4\">\n {/* Service Management Actions */}\n <div>\n <h4 className=\"text-sm font-medium text-gray-700 mb-3\">Service Management</h4>\n <div className=\"space-y-3\">\n {/* SIM Management - Only for SIM services */}\n {isSimService && isActive && (\n <ActionButton\n icon={<Cog6ToothIcon className=\"h-5 w-5 text-blue-600\" />}\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 <ActionButton\n icon={<Cog6ToothIcon className=\"h-5 w-5 text-gray-600\" />}\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 <ActionButton\n icon={<PauseIcon className=\"h-5 w-5 text-yellow-600\" />}\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 <ActionButton\n icon={<PlayIcon className=\"h-5 w-5 text-green-600\" />}\n label=\"Resume Service\"\n description=\"Resume suspended service\"\n variant=\"outline\"\n onClick={handleResume}\n disabled={loading === \"resume\"}\n />\n )}\n </div>\n </div>\n\n {/* Plan Management Actions */}\n {(isActive || isSuspended) && !subscription.cycle.includes(\"One-time\") && (\n <div>\n <h4 className=\"text-sm font-medium text-gray-700 mb-3\">Plan Management</h4>\n <div className=\"space-y-3\">\n <ActionButton\n icon={<ArrowUpIcon className=\"h-5 w-5 text-green-600\" />}\n label=\"Upgrade Plan\"\n description=\"Upgrade to a higher tier plan with more features\"\n onClick={handleUpgrade}\n />\n\n <ActionButton\n icon={<ArrowDownIcon className=\"h-5 w-5 text-blue-600\" />}\n label=\"Downgrade Plan\"\n description=\"Switch to a lower tier plan\"\n onClick={handleDowngrade}\n />\n </div>\n </div>\n )}\n\n {/* Billing Actions */}\n <div>\n <h4 className=\"text-sm font-medium text-gray-700 mb-3\">Billing & Payment</h4>\n <div className=\"space-y-3\">\n <ActionButton\n icon={<DocumentTextIcon className=\"h-5 w-5 text-gray-600\" />}\n label=\"View Invoices\"\n description=\"View billing history and download invoices\"\n onClick={handleViewInvoices}\n />\n\n <ActionButton\n icon={<CreditCardIcon className=\"h-5 w-5 text-gray-600\" />}\n label=\"Manage Payment\"\n description=\"Update payment methods and billing information\"\n onClick={handleManagePayment}\n />\n </div>\n </div>\n\n {/* Cancellation Actions */}\n {!isCancelled && !isPending && (\n <div>\n <h4 className=\"text-sm font-medium text-gray-700 mb-3\">Cancellation</h4>\n <div className=\"bg-red-50 border border-red-200 rounded-lg p-4\">\n <div className=\"flex items-start space-x-3\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-red-600 mt-0.5\" />\n <div className=\"flex-1\">\n <h5 className=\"text-sm font-semibold text-red-900 mb-1\">Cancel Subscription</h5>\n <p className=\"text-sm text-red-800 mb-3\">\n Permanently cancel this subscription. This action cannot be undone and you will\n lose access to the service.\n </p>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={handleCancel}\n loading={loading === \"cancel\"}\n loadingText=\"Cancelling...\"\n leftIcon={<XMarkIcon className=\"h-4 w-4\" />}\n >\n Cancel Subscription\n </Button>\n </div>\n </div>\n </div>\n </div>\n )}\n\n {/* Status Information */}\n {(isCancelled || isPending) && (\n <div className=\"bg-gray-50 border border-gray-200 rounded-lg p-4\">\n <div className=\"flex items-center space-x-2\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-gray-600\" />\n <h5 className=\"text-sm font-semibold text-gray-900\">\n {isCancelled ? \"Subscription Cancelled\" : \"Subscription Pending\"}\n </h5>\n </div>\n <p className=\"text-sm text-gray-700 mt-2\">\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 </p>\n </div>\n )}\n </div>\n </SubCard>\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 <CheckCircleIcon className=\"h-5 w-5 text-green-500\" />;\n case \"Suspended\":\n return <ExclamationTriangleIcon className=\"h-5 w-5 text-yellow-500\" />;\n case \"Pending\":\n return <ClockIcon className=\"h-5 w-5 text-blue-500\" />;\n case \"Cancelled\":\n case \"Terminated\":\n return <XCircleIcon className=\"h-5 w-5 text-gray-500\" />;\n default:\n return <ClockIcon className=\"h-5 w-5 text-gray-500\" />;\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<HTMLDivElement, SubscriptionCardProps>(\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 <SubCard ref={ref} className={cn(\"hover:shadow-lg transition-all duration-200\", className)}>\n <div className=\"space-y-4\">\n {/* Header */}\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center space-x-3\">\n {getStatusIcon(subscription.status)}\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"text-lg font-semibold text-gray-900 truncate\">\n {subscription.productName}\n </h3>\n <p className=\"text-sm text-gray-500\">Service ID: {subscription.serviceId}</p>\n </div>\n </div>\n <StatusPill\n label={subscription.status}\n variant={getStatusVariant(subscription.status)}\n size=\"sm\"\n />\n </div>\n\n {/* Details */}\n <div className=\"grid grid-cols-2 gap-4 text-sm\">\n <div>\n <p className=\"text-gray-500\">Price</p>\n <p className=\"font-semibold text-gray-900\">\n {formatCurrency(subscription.amount, {\n currency: \"JPY\",\n locale: getCurrencyLocale(\"JPY\"),\n })}\n </p>\n <p className=\"text-xs text-gray-500\">{getBillingCycleLabel(subscription.cycle)}</p>\n </div>\n <div>\n <p className=\"text-gray-500\">Next Due</p>\n <div className=\"flex items-center space-x-1\">\n <CalendarIcon className=\"h-4 w-4 text-gray-400\" />\n <p className=\"font-medium text-gray-900\">{formatDate(subscription.nextDue)}</p>\n </div>\n </div>\n </div>\n\n {/* Actions */}\n {showActions && (\n <div className=\"flex items-center justify-between pt-2 border-t border-gray-100\">\n <p className=\"text-xs text-gray-500\">\n Created {formatDate(subscription.registrationDate)}\n </p>\n <div className=\"flex items-center space-x-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleViewClick}\n rightIcon={<ArrowTopRightOnSquareIcon className=\"h-4 w-4\" />}\n >\n View\n </Button>\n </div>\n </div>\n )}\n </div>\n </SubCard>\n );\n }\n\n // List variant (default)\n return (\n <SubCard ref={ref} className={cn(\"hover:shadow-md transition-all duration-200\", className)}>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center space-x-4 min-w-0 flex-1\">\n {getStatusIcon(subscription.status)}\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center space-x-3\">\n <h3 className=\"text-base font-semibold text-gray-900 truncate\">\n {subscription.productName}\n </h3>\n <StatusPill\n label={subscription.status}\n variant={getStatusVariant(subscription.status)}\n size=\"sm\"\n />\n </div>\n <p className=\"text-sm text-gray-500 mt-1\">Service ID: {subscription.serviceId}</p>\n </div>\n </div>\n\n <div className=\"flex items-center space-x-6 text-sm\">\n <div className=\"text-right\">\n <p className=\"font-semibold text-gray-900\">\n {formatCurrency(subscription.amount, {\n currency: \"JPY\",\n locale: getCurrencyLocale(\"JPY\"),\n })}\n </p>\n <p className=\"text-gray-500\">{getBillingCycleLabel(subscription.cycle)}</p>\n </div>\n\n <div className=\"text-right\">\n <div className=\"flex items-center space-x-1\">\n <CalendarIcon className=\"h-4 w-4 text-gray-400\" />\n <p className=\"text-gray-900\">{formatDate(subscription.nextDue)}</p>\n </div>\n <p className=\"text-gray-500\">Next due</p>\n </div>\n\n {showActions && (\n <div className=\"flex items-center space-x-2\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={handleViewClick}\n rightIcon={<ArrowTopRightOnSquareIcon className=\"h-4 w-4\" />}\n >\n View\n </Button>\n </div>\n )}\n </div>\n </div>\n </SubCard>\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 <div className=\"bg-yellow-50 border border-yellow-200 rounded p-3\">\n <div className=\"text-sm font-medium text-yellow-900 mb-1\">{title}</div>\n <div className=\"text-sm text-yellow-800\">{children}</div>\n </div>\n );\n}\n\nfunction InfoRow({ label, value }: { label: string; value: string }) {\n return (\n <div>\n <div className=\"text-xs text-gray-500\">{label}</div>\n <div className=\"text-sm font-medium text-gray-900\">{value}</div>\n </div>\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<Step>(1);\n const [loading, setLoading] = useState(false);\n const [details, setDetails] = useState<SimDetails | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [message, setMessage] = useState<string | null>(null);\n const [acceptTerms, setAcceptTerms] = useState(false);\n const [confirmMonthEnd, setConfirmMonthEnd] = useState(false);\n const [cancelMonth, setCancelMonth] = useState<string>(\"\");\n const [email, setEmail] = useState<string>(\"\");\n const [email2, setEmail2] = useState<string>(\"\");\n const [notes, setNotes] = useState<string>(\"\");\n const [registeredEmail, setRegisteredEmail] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchDetails = async () => {\n try {\n const info = await simActionsService.getSimInfo<SimDetails, any>(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 <div className=\"max-w-3xl mx-auto p-6\">\n <div className=\"mb-4\">\n <Link\n href={`/subscriptions/${subscriptionId}#sim-management`}\n className=\"text-blue-600 hover:text-blue-700\"\n >\n ← Back to SIM Management\n </Link>\n <div className=\"text-sm text-gray-500\">Step {step} of 3</div>\n </div>\n\n {error && (\n <div className=\"text-red-700 bg-red-50 border border-red-200 rounded p-3\">{error}</div>\n )}\n {message && (\n <div className=\"text-green-700 bg-green-50 border border-green-200 rounded p-3\">\n {message}\n </div>\n )}\n\n <div className=\"bg-white rounded-xl border border-gray-200 p-6\">\n <h1 className=\"text-xl font-semibold text-gray-900 mb-2\">Cancel SIM</h1>\n <p className=\"text-sm text-gray-600 mb-6\">\n Cancel SIM: Permanently cancel your SIM service. This action cannot be undone and will\n terminate your service immediately.\n </p>\n\n {step === 1 && (\n <div className=\"space-y-6\">\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4 items-end\">\n <InfoRow label=\"SIM\" value={details?.msisdn || \"—\"} />\n <InfoRow label=\"Start Date\" value={details?.startDate || \"—\"} />\n <div>\n <label className=\"block text-sm font-medium text-gray-700 mb-1\">\n Cancellation Month\n </label>\n <select\n value={cancelMonth}\n onChange={e => {\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 <option value=\"\">Select month…</option>\n {monthOptions.map(opt => (\n <option key={opt.value} value={opt.value}>\n {opt.label}\n </option>\n ))}\n </select>\n <p className=\"text-xs text-gray-500 mt-1\">\n Cancellation takes effect at the start of the selected month.\n </p>\n </div>\n </div>\n <div className=\"flex justify-end\">\n <button\n disabled={!canProceedStep2}\n onClick={() => setStep(2)}\n className=\"px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n\n {step === 2 && (\n <div className=\"space-y-6\">\n <div className=\"space-y-3\">\n <Notice title=\"Cancellation Procedure\">\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 </Notice>\n <Notice title=\"Minimum Contract Term\">\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 </Notice>\n <Notice title=\"Option Services\">\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 </Notice>\n <Notice title=\"MNP Transfer (Voice Plans)\">\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 </Notice>\n </div>\n <div className=\"flex items-center gap-2\">\n <input\n id=\"acceptTerms\"\n type=\"checkbox\"\n checked={acceptTerms}\n onChange={e => setAcceptTerms(e.target.checked)}\n />\n <label htmlFor=\"acceptTerms\" className=\"text-sm text-gray-700\">\n I have read and accepted the conditions above.\n </label>\n </div>\n <div className=\"flex items-start gap-2\">\n <input\n id=\"confirmMonthEnd\"\n type=\"checkbox\"\n checked={confirmMonthEnd}\n onChange={e => setConfirmMonthEnd(e.target.checked)}\n disabled={!cancelMonth}\n />\n <label htmlFor=\"confirmMonthEnd\" className=\"text-sm text-gray-700\">\n I would like to cancel my SonixNet SIM subscription at the end of the selected month\n above.\n </label>\n </div>\n <div className=\"flex justify-between\">\n <button\n onClick={() => 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 </button>\n <button\n disabled={!canProceedStep3}\n onClick={() => setStep(3)}\n className=\"px-4 py-2 rounded-md bg-blue-600 text-white text-sm disabled:opacity-50\"\n >\n Next\n </button>\n </div>\n </div>\n )}\n\n {step === 3 && (\n <div className=\"space-y-6\">\n {registeredEmail && (\n <div className=\"text-sm text-gray-800\">\n Your registered email address is:{\" \"}\n <span className=\"font-medium\">{registeredEmail}</span>\n </div>\n )}\n <div className=\"text-sm text-gray-700\">\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 </div>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <label className=\"block text-sm font-medium text-gray-700\">Email address</label>\n <input\n className=\"mt-1 w-full border border-gray-300 rounded-md px-3 py-2 text-sm\"\n value={email}\n onChange={e => setEmail(e.target.value)}\n placeholder=\"you@example.com\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-gray-700\">(Confirm)</label>\n <input\n className=\"mt-1 w-full border border-gray-300 rounded-md px-3 py-2 text-sm\"\n value={email2}\n onChange={e => setEmail2(e.target.value)}\n placeholder=\"you@example.com\"\n />\n </div>\n <div className=\"md:col-span-2\">\n <label className=\"block text-sm font-medium text-gray-700\">\n If you have any other questions/comments/requests regarding your cancellation,\n please note them below and an Assist Solutions staff will contact you shortly.\n </label>\n <textarea\n className=\"mt-1 w-full border border-gray-300 rounded-md px-3 py-2 text-sm\"\n rows={4}\n value={notes}\n onChange={e => setNotes(e.target.value)}\n placeholder=\"If you have any questions or requests, note them here.\"\n />\n </div>\n </div>\n {emailProvided && !emailValid && (\n <div className=\"text-xs text-red-600\">\n Please enter a valid email address in both fields.\n </div>\n )}\n {emailProvided && emailValid && !emailsMatch && (\n <div className=\"text-xs text-red-600\">Email addresses do not match.</div>\n )}\n <div className=\"text-sm text-gray-700\">\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 </div>\n <div className=\"flex justify-between\">\n <button\n onClick={() => 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 </button>\n <button\n onClick={() => {\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 </button>\n </div>\n </div>\n )}\n </div>\n </div>\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 <CheckCircleIcon className=\"h-6 w-6 text-green-500\" />;\n case \"Suspended\":\n return <ExclamationTriangleIcon className=\"h-6 w-6 text-yellow-500\" />;\n case \"Terminated\":\n return <XCircleIcon className=\"h-6 w-6 text-red-500\" />;\n case \"Cancelled\":\n return <XCircleIcon className=\"h-6 w-6 text-gray-500\" />;\n case \"Pending\":\n return <ClockIcon className=\"h-6 w-6 text-blue-500\" />;\n default:\n return <ServerIcon className=\"h-6 w-6 text-gray-500\" />;\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 <CheckCircleIcon className=\"h-5 w-5 text-green-500\" />;\n case \"Overdue\":\n return <ExclamationTriangleIcon className=\"h-5 w-5 text-red-500\" />;\n case \"Unpaid\":\n return <ClockIcon className=\"h-5 w-5 text-yellow-500\" />;\n default:\n return <DocumentTextIcon className=\"h-5 w-5 text-gray-500\" />;\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 <div className=\"flex items-center justify-center h-64\">\n <div className=\"text-center space-y-3\">\n <LoadingSpinner size=\"lg\" />\n <p className=\"mt-2 text-gray-600\">Loading subscription...</p>\n </div>\n </div>\n );\n }\n\n if (error || !subscription) {\n return (\n <div className=\"space-y-6\">\n <div className=\"bg-red-50 border border-red-200 rounded-md p-4\">\n <div className=\"flex\">\n <div className=\"flex-shrink-0\">\n <ExclamationTriangleIcon className=\"h-5 w-5 text-red-400\" />\n </div>\n <div className=\"ml-3\">\n <h3 className=\"text-sm font-medium text-red-800\">Error loading subscription</h3>\n <div className=\"mt-2 text-sm text-red-700\">\n {error instanceof Error ? error.message : \"Subscription not found\"}\n </div>\n <div className=\"mt-4\">\n <Link href=\"/subscriptions\" className=\"text-red-700 hover:text-red-600 font-medium\">\n ← Back to subscriptions\n </Link>\n </div>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"py-6\">\n <div className=\"max-w-7xl mx-auto px-4 sm:px-6 md:px-8\">\n <div className=\"mb-8\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center\">\n <Link href=\"/subscriptions\" className=\"mr-4 text-gray-600 hover:text-gray-900\">\n <ArrowLeftIcon className=\"h-6 w-6\" />\n </Link>\n <div className=\"flex items-center\">\n <ServerIcon className=\"h-8 w-8 text-blue-600 mr-3\" />\n <div>\n <h1 className=\"text-2xl font-bold text-gray-900\">{subscription.productName}</h1>\n <p className=\"text-gray-600\">Service ID: {subscription.serviceId}</p>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <SubCard className=\"mb-6\">\n <DetailHeader\n title=\"Subscription Details\"\n subtitle=\"Service subscription information\"\n leftIcon={getStatusIcon(subscription.status)}\n status={{\n label: subscription.status,\n variant:\n subscription.status === \"Active\"\n ? \"success\"\n : subscription.status === \"Suspended\"\n ? \"warning\"\n : [\"Cancelled\", \"Terminated\"].includes(subscription.status)\n ? \"neutral\"\n : \"info\",\n }}\n />\n <div className=\"pt-4\">\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n <div>\n <h4 className=\"text-sm font-medium text-gray-500 uppercase tracking-wider\">\n Billing Amount\n </h4>\n <p className=\"mt-2 text-2xl font-bold text-gray-900\">\n {formatCurrency(subscription.amount)}\n </p>\n <p className=\"text-sm text-gray-500\">{formatBillingLabel(subscription.cycle)}</p>\n </div>\n <div>\n <h4 className=\"text-sm font-medium text-gray-500 uppercase tracking-wider\">\n Next Due Date\n </h4>\n <p className=\"mt-2 text-lg text-gray-900\">{formatDate(subscription.nextDue)}</p>\n <div className=\"flex items-center mt-1\">\n <CalendarIcon className=\"h-4 w-4 text-gray-400 mr-1\" />\n <span className=\"text-sm text-gray-500\">Due date</span>\n </div>\n </div>\n <div>\n <h4 className=\"text-sm font-medium text-gray-500 uppercase tracking-wider\">\n Registration Date\n </h4>\n <p className=\"mt-2 text-lg text-gray-900\">\n {formatDate(subscription.registrationDate)}\n </p>\n <span className=\"text-sm text-gray-500\">Service created</span>\n </div>\n </div>\n </div>\n </SubCard>\n\n {subscription.productName.toLowerCase().includes(\"sim\") && (\n <div className=\"mb-8\">\n <SubCard>\n <div className=\"flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0\">\n <div>\n <h3 className=\"text-xl font-semibold text-gray-900\">Service Management</h3>\n <p className=\"text-sm text-gray-600 mt-1\">\n Switch between billing and SIM management views\n </p>\n </div>\n <div className=\"flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 bg-gray-100 rounded-xl p-2\">\n <Link\n href={`/subscriptions/${subscriptionId}#sim-management`}\n className={`px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 min-w-[140px] text-center ${showSimManagement ? \"bg-white text-blue-600 shadow-md hover:shadow-lg\" : \"text-gray-600 hover:text-gray-900 hover:bg-gray-200\"}`}\n >\n <ServerIcon className=\"h-4 w-4 inline mr-2\" />\n SIM Management\n </Link>\n <Link\n href={`/subscriptions/${subscriptionId}`}\n className={`px-6 py-3 text-sm font-semibold rounded-lg transition-all duration-200 min-w-[120px] text-center ${showInvoices ? \"bg-white text-blue-600 shadow-md hover:shadow-lg\" : \"text-gray-600 hover:text-gray-900 hover:bg-gray-200\"}`}\n >\n <DocumentTextIcon className=\"h-4 w-4 inline mr-2\" />\n Invoices\n </Link>\n </div>\n </div>\n </SubCard>\n </div>\n )}\n\n {showSimManagement && (\n <div className=\"mb-10\">\n <SimManagementSection subscriptionId={subscriptionId} />\n </div>\n )}\n\n {showInvoices && <InvoicesList subscriptionId={subscriptionId} />}\n </div>\n </div>\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<SubscriptionList | Subscription[]>({\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<SubscriptionList | Subscription[]>(\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<Subscription[]>({\n queryKey: [\"subscriptions\", \"active\"],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const res = await apiClient.get<Subscription[]>(`/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<Subscription>({\n queryKey: [\"subscription\", subscriptionId],\n queryFn: async () => {\n if (!token) {\n throw new Error(\"Authentication required\");\n }\n\n const res = await apiClient.get<Subscription>(`/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<InvoiceList>({\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<InvoiceList>(\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<TDetails = any, TUsage = any> {\n details: TDetails;\n usage: TUsage;\n}\n\nexport class SimActionsService {\n async getSimInfo<TDetails = any, TUsage = any>(\n subscriptionId: number\n ): Promise<SimInfo<TDetails, TUsage>> {\n const res = await apiClient.get<SimInfo<TDetails, TUsage>>(\n `/subscriptions/${subscriptionId}/sim`\n );\n return res.data as SimInfo<TDetails, TUsage>;\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<TData = unknown, TError = Error>(\n options: UseQueryOptions<TData, TError> & { 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<TData = unknown, TError = Error>(\n options: UseInfiniteQueryOptions<TData, TError> & { 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<any>,\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<TData>(queryKey: any[]) {\n const queryClient = useQueryClient();\n\n const updateOptimistically = async <TVariables>(\n variables: TVariables,\n updater: (oldData: TData | undefined, variables: TVariables) => TData,\n mutationFn: (variables: TVariables) => Promise<TData>\n ) => {\n // Cancel outgoing refetches\n await queryClient.cancelQueries({ queryKey });\n\n // Snapshot previous value\n const previousData = queryClient.getQueryData<TData>(queryKey);\n\n // Optimistically update\n queryClient.setQueryData<TData>(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<TData>(queryKey, result);\n\n return result;\n } catch (error) {\n // Rollback on error\n queryClient.setQueryData<TData>(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<T, CreateT = Partial<T>, UpdateT = Partial<T>>\n implements CrudService<T, CreateT, UpdateT>\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<string, string | number | boolean> {\n if (!params) return {};\n\n const transformed: Record<string, string | number | boolean> = {};\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<R>(response: ApiResponse<R>): 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<PaginatedResponse<T>> {\n const response = await this.apiClient.get<PaginatedResponse<T>>(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<T> {\n const response = await this.apiClient.get<T>(this.buildPath(id));\n return this.extractData(response);\n }\n\n /**\n * Create new item\n */\n async create(data: CreateT): Promise<T> {\n const response = await this.apiClient.post<T>(this.basePath, data);\n return this.extractData(response);\n }\n\n /**\n * Update existing item\n */\n async update(id: string, data: UpdateT): Promise<T> {\n const response = await this.apiClient.patch<T>(this.buildPath(id), data);\n return this.extractData(response);\n }\n\n /**\n * Delete item\n */\n async delete(id: string): Promise<void> {\n await this.apiClient.delete(this.buildPath(id));\n }\n\n /**\n * Check if item exists\n */\n async exists(id: string): Promise<boolean> {\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<T[]> {\n const response = await this.apiClient.get<T[]>(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<T[]> {\n const response = await this.apiClient.post<T[]>(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<T[]> {\n const response = await this.apiClient.patch<T[]>(this.buildPath(\"bulk\"), { updates });\n return this.extractData(response);\n }\n\n /**\n * Bulk delete items\n */\n async bulkDelete(ids: string[]): Promise<void> {\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<PaginatedResponse<T>> {\n const searchParams = {\n q: query,\n ...params,\n };\n\n const response = await this.apiClient.get<PaginatedResponse<T>>(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<number> {\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<T>,\n UpdateT = Partial<T>,\n> extends BaseService<T, CreateT, UpdateT> {\n /**\n * Get current user's items\n */\n async getMine(params?: QueryParams): Promise<PaginatedResponse<T>> {\n const response = await this.apiClient.get<PaginatedResponse<T>>(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<PaginatedResponse<T>> {\n const searchParams = {\n userId,\n ...params,\n };\n\n const response = await this.apiClient.get<PaginatedResponse<T>>(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<T = unknown> {\n success: boolean;\n data?: T;\n error?: {\n code: string;\n message: string;\n details?: Record<string, unknown>;\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<string, unknown>\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<string, string>;\n}\n\nexport class ApiClient {\n private baseUrl: string;\n private timeout: number;\n private retries: number;\n private defaultHeaders: Record<string, string>;\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<ApiRequestConfig> {\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<T>(response: ApiResponse<T>): Promise<ApiResponse<T>> {\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, string | number | boolean>): 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<ApiError> {\n let errorMessage = `HTTP ${response.status}`;\n let errorCode: string | undefined;\n let errorDetails: Record<string, unknown> | 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<string, unknown>;\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<string, unknown>;\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<T>(\n endpoint: string,\n config: ApiRequestConfig = {}\n ): Promise<ApiResponse<T>> {\n // Apply request interceptors\n const processedConfig = await this.applyRequestInterceptors(config);\n\n const url = this.buildUrl(\n endpoint,\n processedConfig.params as Record<string, string | number | boolean>\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<T> = {\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<T>(endpoint: string, config?: ApiRequestConfig): Promise<ApiResponse<T>> {\n return this.makeRequest<T>(endpoint, { ...config, method: \"GET\" });\n }\n\n /**\n * POST request\n */\n async post<T>(\n endpoint: string,\n data?: unknown,\n config?: ApiRequestConfig\n ): Promise<ApiResponse<T>> {\n return this.makeRequest<T>(endpoint, { ...config, method: \"POST\", data });\n }\n\n /**\n * PUT request\n */\n async put<T>(\n endpoint: string,\n data?: unknown,\n config?: ApiRequestConfig\n ): Promise<ApiResponse<T>> {\n return this.makeRequest<T>(endpoint, { ...config, method: \"PUT\", data });\n }\n\n /**\n * PATCH request\n */\n async patch<T>(\n endpoint: string,\n data?: unknown,\n config?: ApiRequestConfig\n ): Promise<ApiResponse<T>> {\n return this.makeRequest<T>(endpoint, { ...config, method: \"PATCH\", data });\n }\n\n /**\n * DELETE request\n */\n async delete<T>(endpoint: string, config?: ApiRequestConfig): Promise<ApiResponse<T>> {\n return this.makeRequest<T>(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<T = any> = {\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<any> => ({\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<string> => ({\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<string> => ({\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<string> => ({\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<string> => ({\n validate: value => !value || regex.test(value),\n message,\n }),\n\n phone: (message = \"Please enter a valid phone number\"): ValidationRule<string> => ({\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<string> => ({\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<string> => ({\n validate: value => !value || !isNaN(Number(value)),\n message,\n }),\n\n min: (min: number, message?: string): ValidationRule<string | number> => ({\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<string | number> => ({\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<T>(value: T, rules: ValidationRule<T>[]): 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<T extends Record<string, any>>(\n values: T,\n rules: Partial<Record<keyof T, ValidationRule<T[keyof T]>[]>>\n): Record<keyof T, ValidationResult> {\n const results = {} as Record<keyof T, ValidationResult>;\n\n for (const [field, fieldRules] of Object.entries(rules) as [\n keyof T,\n ValidationRule<T[keyof T]>[],\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<T extends Record<string, any>>(\n validationResults: Record<keyof T, ValidationResult>\n): boolean {\n return Object.values(validationResults).every(result => result.isValid);\n}\n\n// Get first error for a field\nexport function getFieldError<T extends Record<string, any>>(\n validationResults: Record<keyof T, ValidationResult>,\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<T = unknown> {\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<T extends Record<string, unknown> = Record<string, unknown>> {\n values: T;\n errors: Partial<Record<keyof T, string>>;\n touched: Partial<Record<keyof T, boolean>>;\n dirty: boolean;\n valid: boolean;\n submitting: boolean;\n submitted: boolean;\n}\n\n// Validation types\nexport type ValidationRule<T = unknown> = (value: T) => string | undefined;\n\nexport interface FieldValidation<T = unknown> {\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<T>[];\n}\n\nexport interface FormValidation<T extends Record<string, unknown> = Record<string, unknown>> {\n fields: Partial<Record<keyof T, FieldValidation>>;\n global?: ValidationRule<T>[];\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<T extends Record<string, unknown> = Record<string, unknown>> {\n initialValues: T;\n validation?: FormValidation<T>;\n onSubmit: (values: T) => void | Promise<void>;\n onChange?: (values: T) => void;\n children: React.ReactNode;\n className?: string;\n testId?: string;\n}\n\nexport interface FieldProps<T = unknown> {\n name: string;\n children: (field: FormField<T>) => React.ReactNode;\n}\n\nexport interface FieldArrayProps<T = unknown> {\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<T extends Record<string, unknown> = Record<string, unknown>> {\n initialValues: T;\n validation?: FormValidation<T>;\n onSubmit?: (values: T) => void | Promise<void>;\n onChange?: (values: T) => void;\n validateOnChange?: boolean;\n validateOnBlur?: boolean;\n}\n\nexport interface UseFormReturn<T extends Record<string, unknown> = Record<string, unknown>> {\n values: T;\n errors: Partial<Record<keyof T, string>>;\n touched: Partial<Record<keyof T, boolean>>;\n dirty: boolean;\n valid: boolean;\n submitting: boolean;\n submitted: boolean;\n setValue: <K extends keyof T>(name: K, value: T[K]) => void;\n setError: <K extends keyof T>(name: K, error: string) => void;\n setTouched: <K extends keyof T>(name: K, touched: boolean) => void;\n setFieldValue: <K extends keyof T>(name: K, value: T[K]) => void;\n setFieldError: <K extends keyof T>(name: K, error: string) => void;\n setFieldTouched: <K extends keyof T>(name: K, touched: boolean) => void;\n validateField: <K extends keyof T>(name: K) => void;\n validateForm: () => void;\n resetForm: (values?: T) => void;\n submitForm: () => void;\n handleSubmit: (event: React.FormEvent) => void;\n getFieldProps: <K extends keyof T>(name: K) => FormField<T[K]>;\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<string> =>\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<string> =>\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<string> =>\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<string> =>\n value =>\n value && !regex.test(value) ? message : undefined,\n\n min:\n (min: number, message?: string): ValidationRule<number> =>\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<number> =>\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<string> =>\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<string> =>\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<string> =>\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<string> =>\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<BundleMetrics>): 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<T extends ComponentType<any>>(\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<T extends ComponentType<any>>(\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<any>): 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<T extends ComponentType<any>>(\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":[]}]