Update import paths for SubCard and FormField components across multiple files to improve module structure and maintainability.

This commit is contained in:
barsa 2025-09-25 15:14:36 +09:00
parent be3af76e01
commit 5a8e9624ae
24 changed files with 767 additions and 23 deletions

View File

@ -0,0 +1,227 @@
import { Injectable } from "@nestjs/common";
import type {
WhmcsInvoicesResponse,
WhmcsInvoiceResponse,
WhmcsProductsResponse,
WhmcsClientResponse,
WhmcsSsoResponse,
WhmcsValidateLoginResponse,
WhmcsAddClientResponse,
WhmcsCatalogProductsResponse,
WhmcsPayMethodsResponse,
WhmcsAddPayMethodResponse,
WhmcsPaymentGatewaysResponse,
WhmcsCreateInvoiceResponse,
WhmcsUpdateInvoiceResponse,
WhmcsCapturePaymentResponse,
WhmcsAddCreditResponse,
WhmcsAddInvoicePaymentResponse,
WhmcsGetInvoicesParams,
WhmcsGetClientsProductsParams,
WhmcsCreateSsoTokenParams,
WhmcsValidateLoginParams,
WhmcsAddClientParams,
WhmcsGetPayMethodsParams,
WhmcsAddPayMethodParams,
WhmcsCreateInvoiceParams,
WhmcsUpdateInvoiceParams,
WhmcsCapturePaymentParams,
WhmcsAddCreditParams,
WhmcsAddInvoicePaymentParams,
} from "../../types/whmcs-api.types";
import { WhmcsHttpClientService } from "./whmcs-http-client.service";
import { WhmcsConfigService } from "../config/whmcs-config.service";
import type { WhmcsRequestOptions } from "../types/connection.types";
/**
* Service containing all WHMCS API method implementations
* Organized by functional area (clients, invoices, payments, etc.)
*/
@Injectable()
export class WhmcsApiMethodsService {
constructor(
private readonly httpClient: WhmcsHttpClientService,
private readonly configService: WhmcsConfigService
) {}
// ==========================================
// HEALTH CHECK METHODS
// ==========================================
/**
* Perform health check on WHMCS API
*/
async healthCheck(): Promise<boolean> {
try {
await this.makeRequest("GetProducts", { limitnum: 1 });
return true;
} catch {
return false;
}
}
/**
* Check if WHMCS service is available
*/
async isAvailable(): Promise<boolean> {
return this.healthCheck();
}
/**
* Get WHMCS system information
*/
async getSystemInfo(): Promise<unknown> {
return this.makeRequest("GetProducts", { limitnum: 1 });
}
// ==========================================
// CLIENT API METHODS
// ==========================================
async getClientDetails(clientId: number): Promise<WhmcsClientResponse> {
return this.makeRequest<WhmcsClientResponse>("GetClientsDetails", {
clientid: clientId,
stats: true, // Required by some WHMCS versions to include defaultpaymethodid
});
}
async getClientDetailsByEmail(email: string): Promise<WhmcsClientResponse> {
return this.makeRequest<WhmcsClientResponse>("GetClientsDetails", {
email,
stats: true,
});
}
async updateClient(
clientId: number,
updateData: Partial<WhmcsClientResponse["client"]>
): Promise<{ result: string }> {
return this.makeRequest<{ result: string }>("UpdateClient", {
clientid: clientId,
...updateData,
});
}
async addClient(params: WhmcsAddClientParams): Promise<WhmcsAddClientResponse> {
return this.makeRequest("AddClient", params);
}
async validateLogin(params: WhmcsValidateLoginParams): Promise<WhmcsValidateLoginResponse> {
return this.makeRequest("ValidateLogin", params);
}
// ==========================================
// INVOICE API METHODS
// ==========================================
async getInvoices(params: WhmcsGetInvoicesParams = {}): Promise<WhmcsInvoicesResponse> {
return this.makeRequest("GetInvoices", params);
}
async getInvoice(invoiceId: number): Promise<WhmcsInvoiceResponse> {
return this.makeRequest("GetInvoice", { invoiceid: invoiceId });
}
async createInvoice(params: WhmcsCreateInvoiceParams): Promise<WhmcsCreateInvoiceResponse> {
return this.makeRequest("CreateInvoice", params);
}
async updateInvoice(params: WhmcsUpdateInvoiceParams): Promise<WhmcsUpdateInvoiceResponse> {
return this.makeRequest("UpdateInvoice", params);
}
// ==========================================
// PRODUCT/SUBSCRIPTION API METHODS
// ==========================================
async getClientsProducts(params: WhmcsGetClientsProductsParams): Promise<WhmcsProductsResponse> {
return this.makeRequest("GetClientsProducts", params);
}
async getCatalogProducts(): Promise<WhmcsCatalogProductsResponse> {
return this.makeRequest("GetProducts", {});
}
// ==========================================
// PAYMENT API METHODS
// ==========================================
async getPaymentMethods(params: WhmcsGetPayMethodsParams): Promise<WhmcsPayMethodsResponse> {
return this.makeRequest("GetPayMethods", params);
}
async addPaymentMethod(params: WhmcsAddPayMethodParams): Promise<WhmcsAddPayMethodResponse> {
return this.makeRequest("AddPayMethod", params);
}
async getPaymentGateways(): Promise<WhmcsPaymentGatewaysResponse> {
return this.makeRequest("GetPaymentMethods", {});
}
async capturePayment(params: WhmcsCapturePaymentParams): Promise<WhmcsCapturePaymentResponse> {
return this.makeRequest("CapturePayment", params);
}
async addCredit(params: WhmcsAddCreditParams): Promise<WhmcsAddCreditResponse> {
return this.makeRequest("AddCredit", params);
}
async addInvoicePayment(
params: WhmcsAddInvoicePaymentParams
): Promise<WhmcsAddInvoicePaymentResponse> {
return this.makeRequest("AddInvoicePayment", params);
}
// ==========================================
// SSO API METHODS
// ==========================================
async createSsoToken(params: WhmcsCreateSsoTokenParams): Promise<WhmcsSsoResponse> {
return this.makeRequest("CreateSsoToken", params);
}
// ==========================================
// ADMIN API METHODS (require admin auth)
// ==========================================
async acceptOrder(orderId: number): Promise<{ result: string }> {
if (!this.configService.hasAdminAuth()) {
throw new Error("Admin authentication required for AcceptOrder");
}
return this.makeRequest<{ result: string }>(
"AcceptOrder",
{ orderid: orderId },
{ useAdminAuth: true }
);
}
async cancelOrder(orderId: number): Promise<{ result: string }> {
if (!this.configService.hasAdminAuth()) {
throw new Error("Admin authentication required for CancelOrder");
}
return this.makeRequest<{ result: string }>(
"CancelOrder",
{ orderid: orderId },
{ useAdminAuth: true }
);
}
// ==========================================
// PRIVATE HELPER METHODS
// ==========================================
/**
* Make a request using the HTTP client
*/
private async makeRequest<T>(
action: string,
params: Record<string, unknown>,
options: WhmcsRequestOptions = {}
): Promise<T> {
const config = this.configService.getConfig();
const response = await this.httpClient.makeRequest<T>(config, action, params, options);
return response.data as T;
}
}

View File

@ -0,0 +1,209 @@
import { Injectable, NotFoundException, BadRequestException, UnauthorizedException } from "@nestjs/common";
import { getErrorMessage } from "@bff/core/utils/error.util";
import type { WhmcsErrorResponse } from "../../types/whmcs-api.types";
/**
* Service for handling and normalizing WHMCS API errors
* Maps WHMCS errors to appropriate NestJS exceptions
*/
@Injectable()
export class WhmcsErrorHandlerService {
/**
* Handle WHMCS API error response
*/
handleApiError(
errorResponse: WhmcsErrorResponse,
action: string,
params: Record<string, unknown>
): never {
const message = errorResponse.message;
const errorCode = errorResponse.errorcode;
// Normalize common, expected error responses to domain exceptions
if (this.isNotFoundError(action, message)) {
throw this.createNotFoundException(action, message, params);
}
if (this.isAuthenticationError(message, errorCode)) {
throw new UnauthorizedException(`WHMCS Authentication Error: ${message}`);
}
if (this.isValidationError(message, errorCode)) {
throw new BadRequestException(`WHMCS Validation Error: ${message}`);
}
// Generic WHMCS API error
throw new Error(`WHMCS API Error [${action}]: ${message} (${errorCode || 'unknown'})`);
}
/**
* Handle general request errors (network, timeout, etc.)
*/
handleRequestError(error: unknown, action: string, params: Record<string, unknown>): never {
const message = getErrorMessage(error);
if (this.isTimeoutError(error)) {
throw new Error(`WHMCS API timeout [${action}]: Request timed out`);
}
if (this.isNetworkError(error)) {
throw new Error(`WHMCS API network error [${action}]: ${message}`);
}
// Re-throw the original error if it's already a known exception type
if (this.isKnownException(error)) {
throw error;
}
// Generic request error
throw new Error(`WHMCS API request failed [${action}]: ${message}`);
}
/**
* Check if error indicates a not found condition
*/
private isNotFoundError(action: string, message: string): boolean {
const lowerMessage = message.toLowerCase();
// Client not found errors
if (action === "GetClientsDetails" && lowerMessage.includes("client not found")) {
return true;
}
// Invoice not found errors
if ((action === "GetInvoice" || action === "UpdateInvoice") &&
lowerMessage.includes("invoice not found")) {
return true;
}
// Product not found errors
if (action === "GetClientsProducts" && lowerMessage.includes("no products found")) {
return true;
}
return false;
}
/**
* Check if error indicates authentication failure
*/
private isAuthenticationError(message: string, errorCode?: string): boolean {
const lowerMessage = message.toLowerCase();
return lowerMessage.includes("authentication") ||
lowerMessage.includes("unauthorized") ||
lowerMessage.includes("invalid credentials") ||
lowerMessage.includes("access denied") ||
errorCode === "AUTHENTICATION_FAILED";
}
/**
* Check if error indicates validation failure
*/
private isValidationError(message: string, errorCode?: string): boolean {
const lowerMessage = message.toLowerCase();
return lowerMessage.includes("required") ||
lowerMessage.includes("invalid") ||
lowerMessage.includes("missing") ||
lowerMessage.includes("validation") ||
errorCode === "VALIDATION_ERROR";
}
/**
* Create appropriate NotFoundException based on action and params
*/
private createNotFoundException(
action: string,
message: string,
params: Record<string, unknown>
): NotFoundException {
if (action === "GetClientsDetails") {
const emailParam = params["email"];
if (typeof emailParam === "string") {
return new NotFoundException(`Client with email ${emailParam} not found`);
}
const clientIdParam = params["clientid"];
const identifier =
typeof clientIdParam === "string" || typeof clientIdParam === "number"
? clientIdParam
: "unknown";
return new NotFoundException(`Client with ID ${identifier} not found`);
}
if (action === "GetInvoice" || action === "UpdateInvoice") {
const invoiceIdParam = params["invoiceid"];
const identifier =
typeof invoiceIdParam === "string" || typeof invoiceIdParam === "number"
? invoiceIdParam
: "unknown";
return new NotFoundException(`Invoice with ID ${identifier} not found`);
}
// Generic not found error
return new NotFoundException(message);
}
/**
* Check if error is a timeout error
*/
private isTimeoutError(error: unknown): boolean {
const message = getErrorMessage(error).toLowerCase();
return message.includes("timeout") ||
message.includes("aborted") ||
(error instanceof Error && error.name === "AbortError");
}
/**
* Check if error is a network error
*/
private isNetworkError(error: unknown): boolean {
const message = getErrorMessage(error).toLowerCase();
return message.includes("network") ||
message.includes("connection") ||
message.includes("econnrefused") ||
message.includes("enotfound") ||
message.includes("fetch");
}
/**
* Check if error is already a known NestJS exception
*/
private isKnownException(error: unknown): boolean {
return error instanceof NotFoundException ||
error instanceof BadRequestException ||
error instanceof UnauthorizedException;
}
/**
* Get user-friendly error message for client consumption
*/
getUserFriendlyMessage(error: unknown): string {
if (error instanceof NotFoundException) {
return "The requested resource was not found.";
}
if (error instanceof BadRequestException) {
return "The request contains invalid data.";
}
if (error instanceof UnauthorizedException) {
return "Authentication failed. Please check your credentials.";
}
const message = getErrorMessage(error).toLowerCase();
if (message.includes("timeout")) {
return "The request timed out. Please try again.";
}
if (message.includes("network") || message.includes("connection")) {
return "Network error. Please check your connection and try again.";
}
return "An unexpected error occurred. Please try again later.";
}
}

View File

@ -0,0 +1,308 @@
import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { getErrorMessage } from "@bff/core/utils/error.util";
import type {
WhmcsApiResponse,
WhmcsErrorResponse
} from "../../types/whmcs-api.types";
import type {
WhmcsApiConfig,
WhmcsRequestOptions,
WhmcsConnectionStats
} from "../types/connection.types";
/**
* Service for handling HTTP requests to WHMCS API
* Manages timeouts, retries, and response parsing
*/
@Injectable()
export class WhmcsHttpClientService {
private stats: WhmcsConnectionStats = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
};
constructor(@Inject(Logger) private readonly logger: Logger) {}
/**
* Make HTTP request to WHMCS API
*/
async makeRequest<T>(
config: WhmcsApiConfig,
action: string,
params: Record<string, unknown>,
options: WhmcsRequestOptions = {}
): Promise<WhmcsApiResponse<T>> {
const startTime = Date.now();
this.stats.totalRequests++;
this.stats.lastRequestTime = new Date();
try {
const response = await this.executeRequest<T>(config, action, params, options);
const responseTime = Date.now() - startTime;
this.updateSuccessStats(responseTime);
return response;
} catch (error) {
this.stats.failedRequests++;
this.stats.lastErrorTime = new Date();
this.logger.error(`WHMCS HTTP request failed [${action}]`, {
error: getErrorMessage(error),
action,
params: this.sanitizeLogParams(params),
responseTime: Date.now() - startTime,
});
throw error;
}
}
/**
* Get connection statistics
*/
getStats(): WhmcsConnectionStats {
return { ...this.stats };
}
/**
* Reset connection statistics
*/
resetStats(): void {
this.stats = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
averageResponseTime: 0,
};
}
/**
* Execute the actual HTTP request with retry logic
*/
private async executeRequest<T>(
config: WhmcsApiConfig,
action: string,
params: Record<string, unknown>,
options: WhmcsRequestOptions
): Promise<WhmcsApiResponse<T>> {
const maxAttempts = options.retryAttempts ?? config.retryAttempts ?? 3;
let lastError: Error;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await this.performSingleRequest<T>(config, action, params, options);
} catch (error) {
lastError = error as Error;
if (attempt === maxAttempts) {
break;
}
// Don't retry on certain error types
if (this.shouldNotRetry(error)) {
break;
}
const delay = this.calculateRetryDelay(attempt, config.retryDelay ?? 1000);
this.logger.warn(`WHMCS request failed, retrying in ${delay}ms`, {
action,
attempt,
maxAttempts,
error: getErrorMessage(error),
});
await this.sleep(delay);
}
}
throw lastError!;
}
/**
* Perform a single HTTP request
*/
private async performSingleRequest<T>(
config: WhmcsApiConfig,
action: string,
params: Record<string, unknown>,
options: WhmcsRequestOptions
): Promise<WhmcsApiResponse<T>> {
const timeout = options.timeout ?? config.timeout ?? 30000;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const requestBody = this.buildRequestBody(config, action, params, options);
const url = `${config.baseUrl}/includes/api.php`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "CustomerPortal/1.0",
},
body: requestBody,
signal: controller.signal,
});
clearTimeout(timeoutId);
const responseText = await response.text();
if (!response.ok) {
const snippet = responseText?.slice(0, 300);
throw new Error(
`HTTP ${response.status}: ${response.statusText}${snippet ? ` | Body: ${snippet}` : ""}`
);
}
return this.parseResponse<T>(responseText, action, params);
} finally {
clearTimeout(timeoutId);
}
}
/**
* Build request body for WHMCS API
*/
private buildRequestBody(
config: WhmcsApiConfig,
action: string,
params: Record<string, unknown>,
options: WhmcsRequestOptions
): string {
const formData = new URLSearchParams();
// Add authentication
if (options.useAdminAuth && config.adminUsername && config.adminPasswordHash) {
formData.append("username", config.adminUsername);
formData.append("password", config.adminPasswordHash);
} else {
formData.append("identifier", config.identifier);
formData.append("secret", config.secret);
}
// Add action and response format
formData.append("action", action);
formData.append("responsetype", "json");
// Add parameters
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
formData.append(key, String(value));
}
}
return formData.toString();
}
/**
* Parse WHMCS API response
*/
private parseResponse<T>(
responseText: string,
action: string,
params: Record<string, unknown>
): WhmcsApiResponse<T> {
let data: WhmcsApiResponse<T>;
try {
data = JSON.parse(responseText) as WhmcsApiResponse<T>;
} catch (parseError) {
this.logger.error(`Invalid JSON response from WHMCS API [${action}]`, {
responseText: responseText.substring(0, 500),
parseError: getErrorMessage(parseError),
params: this.sanitizeLogParams(params),
});
throw new Error("Invalid JSON response from WHMCS API");
}
if (data.result === "error") {
const errorResponse = data as WhmcsErrorResponse;
throw new Error(`WHMCS API Error: ${errorResponse.message} (${errorResponse.errorcode || 'unknown'})`);
}
return data;
}
/**
* Check if error should not be retried
*/
private shouldNotRetry(error: unknown): boolean {
const message = getErrorMessage(error).toLowerCase();
// Don't retry authentication errors
if (message.includes('authentication') || message.includes('unauthorized')) {
return true;
}
// Don't retry validation errors
if (message.includes('invalid') || message.includes('required')) {
return true;
}
// Don't retry not found errors
if (message.includes('not found')) {
return true;
}
return false;
}
/**
* Calculate retry delay with exponential backoff
*/
private calculateRetryDelay(attempt: number, baseDelay: number): number {
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
const jitter = Math.random() * 0.1 * exponentialDelay;
return Math.min(exponentialDelay + jitter, 10000); // Max 10 seconds
}
/**
* Sleep for specified milliseconds
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Update success statistics
*/
private updateSuccessStats(responseTime: number): void {
this.stats.successfulRequests++;
// Update average response time
const totalSuccessful = this.stats.successfulRequests;
this.stats.averageResponseTime =
(this.stats.averageResponseTime * (totalSuccessful - 1) + responseTime) / totalSuccessful;
}
/**
* Sanitize parameters for logging (remove sensitive data)
*/
private sanitizeLogParams(params: Record<string, unknown>): Record<string, unknown> {
const sensitiveKeys = [
'password', 'secret', 'token', 'key', 'auth',
'credit_card', 'cvv', 'ssn', 'social_security'
];
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(params)) {
const keyLower = key.toLowerCase();
const isSensitive = sensitiveKeys.some(sensitive => keyLower.includes(sensitive));
if (isSensitive) {
sanitized[key] = '[REDACTED]';
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}

View File

@ -1,6 +1,6 @@
"use client";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
interface PasswordChangeCardProps {
isChanging: boolean;

View File

@ -1,6 +1,6 @@
"use client";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { UserIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
import type { ProfileEditFormData } from "@customer-portal/domain";

View File

@ -2,7 +2,7 @@
import { useCallback } from "react";
import { Button, Input, ErrorMessage } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField";
import { FormField } from "@/components/molecules/FormField/FormField";
import { useWhmcsLink } from "@/features/auth/hooks";
import {
linkWhmcsRequestSchema,

View File

@ -8,7 +8,7 @@
import { useCallback } from "react";
import Link from "next/link";
import { Button, Input, ErrorMessage } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField";
import { FormField } from "@/components/molecules/FormField/FormField";
import { useLogin } from "../../hooks/use-auth";
import {
loginFormSchema,

View File

@ -8,7 +8,7 @@
import { useEffect } from "react";
import Link from "next/link";
import { Button, Input, ErrorMessage } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField";
import { FormField } from "@/components/molecules/FormField/FormField";
import { usePasswordReset } from "../../hooks/use-auth";
import { useZodForm } from "@customer-portal/validation";
import {

View File

@ -8,7 +8,7 @@
import { useEffect } from "react";
import Link from "next/link";
import { Button, Input, ErrorMessage } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField";
import { FormField } from "@/components/molecules/FormField/FormField";
import { useWhmcsLink } from "../../hooks/use-auth";
import { useZodForm } from "@customer-portal/validation";
import {

View File

@ -6,7 +6,7 @@
"use client";
import { Input } from "@/components/atoms";
import { FormField } from "@/components/molecules/FormField";
import { FormField } from "@/components/molecules/FormField/FormField";
interface AccountStepProps {
formData: {

View File

@ -1,7 +1,7 @@
"use client";
import React from "react";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { formatCurrency } from "@customer-portal/domain";
import type { InvoiceItem } from "@customer-portal/domain";

View File

@ -1,7 +1,7 @@
"use client";
import React from "react";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { formatCurrency } from "@customer-portal/domain";
interface InvoiceTotalsProps {

View File

@ -1,10 +1,10 @@
"use client";
import React, { useMemo, useState } from "react";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { LoadingTable } from "@/components/atoms/loading-skeleton";
import { AsyncBlock } from "@/components/molecules/AsyncBlock";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
import { PaginationBar } from "@/components/molecules/PaginationBar";
import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable";
import { useInvoices } from "@/features/billing/hooks/useBilling";

View File

@ -3,7 +3,7 @@
import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
import { ErrorState } from "@/components/atoms/error-state";
import { CheckCircleIcon, ExclamationTriangleIcon } from "@heroicons/react/24/outline";

View File

@ -3,7 +3,7 @@
import { useState, useEffect } from "react";
import { PageLayout } from "@/components/templates/PageLayout";
import { ErrorBoundary } from "@/components/molecules";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { useSession } from "@/features/auth/hooks";
import { useAuthStore } from "@/features/auth/services/auth.store";

View File

@ -3,7 +3,7 @@
import { Skeleton } from "@/components/atoms";
import { AlertBanner } from "@/components/molecules/AlertBanner";
import { Button } from "@/components/atoms/button";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { useState, useEffect, useCallback } from "react";
import { accountService } from "@/features/account/services/account.service";

View File

@ -1,7 +1,7 @@
"use client";
import { useCheckout } from "@/features/checkout/hooks/useCheckout";
import { PageLayout } from "@/components/templates/PageLayout";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { Button } from "@/components/atoms/button";
import { AlertBanner } from "@/components/molecules/AlertBanner";
import { PageAsync } from "@/components/molecules/AsyncBlock";

View File

@ -11,7 +11,7 @@ import {
LockClosedIcon,
CubeIcon,
} from "@heroicons/react/24/outline";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { StatusPill } from "@/components/atoms/status-pill";
import { ordersService } from "@/features/orders/services/orders.service";
import { calculateOrderTotals, deriveOrderStatusDescriptor, getServiceCategory } from "@/features/orders/utils/order-presenters";

View File

@ -14,7 +14,7 @@ import {
} from "@heroicons/react/24/outline";
import { StatusPill } from "@/components/atoms/status-pill";
import { Button } from "@/components/atoms/button";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain";
import type { Subscription } from "@customer-portal/domain";
import { cn } from "@/lib/utils";

View File

@ -14,7 +14,7 @@ import {
TagIcon,
} from "@heroicons/react/24/outline";
import { StatusPill } from "@/components/atoms/status-pill";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain";
import type { Subscription } from "@customer-portal/domain";
import { cn } from "@/lib/utils";

View File

@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { PageLayout } from "@/components/templates/PageLayout";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
import { AlertBanner } from "@/components/molecules/AlertBanner";

View File

@ -4,7 +4,7 @@ import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { PageLayout } from "@/components/templates/PageLayout";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { simActionsService } from "@/features/subscriptions/services/sim-actions.service";
import { AlertBanner } from "@/components/molecules/AlertBanner";
import { DevicePhoneMobileIcon } from "@heroicons/react/24/outline";

View File

@ -1,7 +1,7 @@
"use client";
import { LoadingCard, Skeleton } from "@/components/atoms/loading-skeleton";
import { SubCard } from "@/components/molecules/SubCard";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { DetailHeader } from "@/components/molecules/DetailHeader";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation";

View File

@ -8,8 +8,8 @@ import { ErrorBoundary } from "@/components/molecules";
import { PageLayout } from "@/components/templates/PageLayout";
import { DataTable } from "@/components/molecules/DataTable/DataTable";
import { StatusPill } from "@/components/atoms/status-pill";
import { SubCard } from "@/components/molecules/SubCard";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar";
import { SubCard } from "@/components/molecules/SubCard/SubCard";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
import { LoadingTable } from "@/components/atoms/loading-skeleton";
import { ErrorState } from "@/components/atoms/error-state";
import { AsyncBlock } from "@/components/molecules/AsyncBlock";