Refactor Subscription Module and Enhance Subscription Detail View

- Updated the subscriptions module to register CancellationController alongside SimController, ensuring more specific route matching for cancellation actions.
- Enhanced SubscriptionDetail component by adding a header action button for canceling Internet services, improving user experience and navigation.
- Removed deprecated Internet service actions from the SubscriptionDetail view, streamlining the component and promoting cleaner code.
This commit is contained in:
barsa 2026-01-05 18:35:05 +09:00
parent b19c213931
commit 655d26da4f
16 changed files with 273 additions and 257 deletions

View File

@ -14,8 +14,10 @@ import { SimManagementModule } from "./sim-management/sim-management.module.js";
import { InternetManagementModule } from "./internet-management/internet-management.module.js"; import { InternetManagementModule } from "./internet-management/internet-management.module.js";
import { CallHistoryModule } from "./call-history/call-history.module.js"; import { CallHistoryModule } from "./call-history/call-history.module.js";
import { CancellationModule } from "./cancellation/cancellation.module.js"; import { CancellationModule } from "./cancellation/cancellation.module.js";
// Import SimController to register it directly in this module before SubscriptionsController // Import controllers to register them directly in this module before SubscriptionsController
// This ensures more specific routes (like :id/sim, :id/cancel) are matched before :id
import { SimController } from "./sim-management/sim.controller.js"; import { SimController } from "./sim-management/sim.controller.js";
import { CancellationController } from "./cancellation/cancellation.controller.js";
@Module({ @Module({
imports: [ imports: [
@ -24,16 +26,19 @@ import { SimController } from "./sim-management/sim.controller.js";
MappingsModule, MappingsModule,
FreebitModule, FreebitModule,
EmailModule, EmailModule,
// Import SimManagementModule for its services and other controllers
SimManagementModule, SimManagementModule,
InternetManagementModule, InternetManagementModule,
CallHistoryModule, CallHistoryModule,
CancellationModule, CancellationModule,
], ],
// Register SimController BEFORE SubscriptionsController to ensure more specific routes // Register specific route controllers BEFORE SubscriptionsController
// (like :id/sim) are matched before less specific routes (like :id) // to ensure routes like :id/sim and :id/cancel are matched before :id
// This fixes the 404 error for /api/subscriptions/:id/sim controllers: [
controllers: [SimController, SubscriptionsController, SimOrdersController], SimController,
CancellationController,
SubscriptionsController,
SimOrdersController,
],
providers: [ providers: [
SubscriptionsService, SubscriptionsService,
SimManagementService, SimManagementService,

View File

@ -0,0 +1,22 @@
import { RouteLoading } from "@/components/molecules/RouteLoading";
import { Server } from "lucide-react";
import {
SubscriptionStatsCardsSkeleton,
SubscriptionTableSkeleton,
} from "@/components/atoms/loading-skeleton";
export default function AccountSubscriptionsLoading() {
return (
<RouteLoading
icon={<Server />}
title="Subscriptions"
description="View and manage your subscriptions"
mode="content"
>
<div className="space-y-6">
<SubscriptionStatsCardsSkeleton />
<SubscriptionTableSkeleton rows={6} />
</div>
</RouteLoading>
);
}

View File

@ -1,59 +1,21 @@
import { RouteLoading } from "@/components/molecules/RouteLoading"; import { RouteLoading } from "@/components/molecules/RouteLoading";
import { Server } from "lucide-react"; import { Server } from "lucide-react";
import { Skeleton } from "@/components/atoms/loading-skeleton"; import {
SubscriptionDetailStatsSkeleton,
InvoiceListSkeleton,
} from "@/components/atoms/loading-skeleton";
export default function AccountServiceDetailLoading() { export default function SubscriptionDetailLoading() {
return ( return (
<RouteLoading icon={<Server />} title="Service" description="Service details" mode="content"> <RouteLoading
icon={<Server />}
title="Subscription"
description="Loading subscription details..."
mode="content"
>
<div className="space-y-6"> <div className="space-y-6">
{/* Main Subscription Card */} <SubscriptionDetailStatsSkeleton />
<div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden"> <InvoiceListSkeleton rows={5} />
{/* Header */}
<div className="px-6 py-5 border-b border-border">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-4 w-32" />
</div>
</div>
<Skeleton className="h-6 w-24 rounded-full" />
</div>
</div>
{/* Stats Row */}
<div className="px-6 py-5 grid grid-cols-1 md:grid-cols-3 gap-6 bg-muted/20">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i}>
<Skeleton className="h-3 w-24 mb-2" />
<div className="flex items-center gap-2">
<Skeleton className="h-6 w-32" />
</div>
</div>
))}
</div>
</div>
{/* Tabs / Billing History Header */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<Skeleton className="h-7 w-32" />
</div>
{/* Invoice List (Table-like) */}
<div className="bg-card border border-border rounded-xl overflow-hidden">
<div className="divide-y divide-border">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="p-4 flex justify-between items-center">
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="h-4 w-24" />
</div>
))}
</div>
</div>
</div>
</div> </div>
</RouteLoading> </RouteLoading>
); );

View File

@ -1,61 +0,0 @@
import { RouteLoading } from "@/components/molecules/RouteLoading";
import { Server } from "lucide-react";
import { Skeleton } from "@/components/atoms/loading-skeleton";
export default function AccountServicesLoading() {
return (
<RouteLoading
icon={<Server />}
title="Services"
description="View and manage your services"
mode="content"
>
<div className="space-y-6">
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-card rounded-xl border border-border p-5 shadow-sm">
<div className="flex items-center gap-4">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-6 w-12" />
</div>
</div>
</div>
))}
</div>
{/* Search and Table */}
<div className="bg-card rounded-xl border border-border shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-border">
<Skeleton className="h-10 w-full max-w-md rounded-lg" />
</div>
<div className="p-0">
{/* Custom Table Skeleton to match embedded style */}
<div className="w-full">
<div className="border-b border-border p-4">
<div className="grid grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-4 w-20" />
))}
</div>
</div>
<div className="divide-y divide-border">
{Array.from({ length: 5 }).map((_, rowIndex) => (
<div key={rowIndex} className="p-4">
<div className="grid grid-cols-5 gap-4">
{Array.from({ length: 5 }).map((_, colIndex) => (
<Skeleton key={colIndex} className="h-4 w-full" />
))}
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</RouteLoading>
);
}

View File

@ -84,5 +84,117 @@ export function LoadingStats({ count = 4 }: { count?: number }) {
); );
} }
// =============================================================================
// Subscription Skeletons - for consistent loading across route and component
// =============================================================================
/** Stats cards skeleton (3 cards for subscriptions list) */
export function SubscriptionStatsCardsSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="bg-card rounded-xl border border-border p-5 shadow-sm">
<div className="flex items-center gap-4">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-6 w-12" />
</div>
</div>
</div>
))}
</div>
);
}
/** Subscription table skeleton (3 columns: Service, Amount, Next Due) */
export function SubscriptionTableSkeleton({ rows = 6 }: { rows?: number }) {
return (
<div className="bg-card rounded-xl border border-border shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-border">
<Skeleton className="h-10 w-full max-w-md rounded-lg" />
</div>
<div className="w-full">
{/* Header skeleton */}
<div className="bg-muted/50 px-6 py-4 border-b border-border">
<div className="grid grid-cols-3 gap-6">
<Skeleton className="h-3 w-16" />
<Skeleton className="h-3 w-16 ml-auto" />
<Skeleton className="h-3 w-20" />
</div>
</div>
{/* Row skeletons */}
<div className="divide-y divide-border">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="px-6 py-5">
<div className="grid grid-cols-3 gap-6 items-center">
<div className="flex items-center space-x-3">
<Skeleton className="h-5 w-5 rounded-full flex-shrink-0" />
<Skeleton className="h-4 w-48" />
</div>
<div className="text-right">
<Skeleton className="h-4 w-32 ml-auto" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-4" />
<Skeleton className="h-4 w-28" />
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
/** Subscription detail stats skeleton (4 columns) */
export function SubscriptionDetailStatsSkeleton() {
return (
<div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-5 grid grid-cols-2 md:grid-cols-4 gap-6">
<div>
<Skeleton className="h-3 w-24 mb-2" />
<Skeleton className="h-7 w-20 rounded-full" />
</div>
<div>
<Skeleton className="h-3 w-24 mb-2" />
<Skeleton className="h-8 w-28" />
</div>
<div>
<Skeleton className="h-3 w-24 mb-2" />
<Skeleton className="h-6 w-32" />
</div>
<div>
<Skeleton className="h-3 w-28 mb-2" />
<Skeleton className="h-6 w-32" />
</div>
</div>
</div>
);
}
/** Invoice/Billing list skeleton */
export function InvoiceListSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="space-y-4">
<Skeleton className="h-7 w-32" />
<div className="bg-card border border-border rounded-xl overflow-hidden">
<div className="divide-y divide-border">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="p-4 flex justify-between items-center">
<div className="space-y-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-3 w-24" />
</div>
<Skeleton className="h-4 w-24" />
</div>
))}
</div>
</div>
</div>
);
}
// Note: PageLoadingState is now handled by PageLayout component with proper skeleton loading // Note: PageLoadingState is now handled by PageLayout component with proper skeleton loading
// FullPageLoadingState removed - use skeleton loading instead // FullPageLoadingState removed - use skeleton loading instead

View File

@ -2,8 +2,7 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import { MagnifyingGlassIcon, ChevronDownIcon } from "@heroicons/react/24/outline";
import { SubCard } from "@/components/molecules/SubCard/SubCard"; import { Spinner } from "@/components/atoms";
import { LoadingTable } from "@/components/atoms/loading-skeleton";
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock"; import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { PaginationBar } from "@/components/molecules/PaginationBar/PaginationBar"; import { PaginationBar } from "@/components/molecules/PaginationBar/PaginationBar";
import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable"; import { InvoiceTable } from "@/features/billing/components/InvoiceTable/InvoiceTable";
@ -51,9 +50,10 @@ export function InvoicesList({
const invoicesQuery = isSubscriptionMode ? subscriptionInvoicesQuery : allInvoicesQuery; const invoicesQuery = isSubscriptionMode ? subscriptionInvoicesQuery : allInvoicesQuery;
const { data, isLoading, error } = invoicesQuery as { const { data, isLoading, isPending, error } = invoicesQuery as {
data?: { invoices: Invoice[]; pagination?: { totalItems: number; totalPages: number } }; data?: { invoices: Invoice[]; pagination?: { totalItems: number; totalPages: number } };
isLoading: boolean; isLoading: boolean;
isPending: boolean;
error: unknown; error: unknown;
}; };
@ -83,13 +83,25 @@ export function InvoicesList({
[] []
); );
if (isLoading || error) { // Loading state - show centered spinner
if (isPending) {
return ( return (
<SubCard> <div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
<div className="flex items-center justify-center py-16">
<Spinner size="lg" className="text-primary" />
</div>
</div>
);
}
// Error state
if (error) {
return (
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
<AsyncBlock isLoading={false} error={error}> <AsyncBlock isLoading={false} error={error}>
<LoadingTable rows={6} columns={5} /> <div />
</AsyncBlock> </AsyncBlock>
</SubCard> </div>
); );
} }

View File

@ -24,7 +24,9 @@ export function InvoiceDetailContainer() {
const rawInvoiceParam = params.id; const rawInvoiceParam = params.id;
const invoiceIdParam = Array.isArray(rawInvoiceParam) ? rawInvoiceParam[0] : rawInvoiceParam; const invoiceIdParam = Array.isArray(rawInvoiceParam) ? rawInvoiceParam[0] : rawInvoiceParam;
const createSsoLinkMutation = useCreateInvoiceSsoLink(); const createSsoLinkMutation = useCreateInvoiceSsoLink();
const { data: invoice, isLoading, error } = useInvoice(invoiceIdParam ?? ""); const { data: invoice, error } = useInvoice(invoiceIdParam ?? "");
// Simple loading check: show skeleton until we have data or an error
const isLoading = !invoice && !error;
const handleCreateSsoLink = (target: "view" | "download" | "pay" = "view") => { const handleCreateSsoLink = (target: "view" | "download" | "pay" = "view") => {
void (async () => { void (async () => {

View File

@ -295,7 +295,9 @@ export function InternetPlansContainer() {
const servicesBasePath = useServicesBasePath(); const servicesBasePath = useServicesBasePath();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const { user } = useAuthSession(); const { user } = useAuthSession();
const { data, isLoading, error } = useAccountInternetCatalog(); const { data, error } = useAccountInternetCatalog();
// Simple loading check: show skeleton until we have data or an error
const isLoading = !data && !error;
const eligibilityQuery = useInternetEligibility(); const eligibilityQuery = useInternetEligibility();
const eligibilityLoading = eligibilityQuery.isLoading; const eligibilityLoading = eligibilityQuery.isLoading;
const refetchEligibility = eligibilityQuery.refetch; const refetchEligibility = eligibilityQuery.refetch;

View File

@ -133,7 +133,9 @@ export function PublicInternetPlansContent({
heroTitle = "Internet Service Plans", heroTitle = "Internet Service Plans",
heroDescription = "NTT Optical Fiber with full English support", heroDescription = "NTT Optical Fiber with full English support",
}: PublicInternetPlansContentProps) { }: PublicInternetPlansContentProps) {
const { data: servicesCatalog, isLoading, error } = usePublicInternetCatalog(); const { data: servicesCatalog, error } = usePublicInternetCatalog();
// Simple loading check: show skeleton until we have data or an error
const isLoading = !servicesCatalog && !error;
const servicesBasePath = useServicesBasePath(); const servicesBasePath = useServicesBasePath();
const defaultCtaPath = `${servicesBasePath}/internet/configure`; const defaultCtaPath = `${servicesBasePath}/internet/configure`;
const ctaPath = propCtaPath ?? defaultCtaPath; const ctaPath = propCtaPath ?? defaultCtaPath;

View File

@ -19,8 +19,10 @@ import {
export function PublicSimPlansView() { export function PublicSimPlansView() {
const router = useRouter(); const router = useRouter();
const servicesBasePath = useServicesBasePath(); const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = usePublicSimCatalog(); const { data, error } = usePublicSimCatalog();
const plans: SimCatalogProduct[] = useMemo(() => data?.plans ?? [], [data?.plans]); const plans: SimCatalogProduct[] = useMemo(() => data?.plans ?? [], [data?.plans]);
// Simple loading check: show skeleton until we have data or an error
const isLoading = !data && !error;
const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice"); const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice");
const handleSelectPlan = (planSku: string) => { const handleSelectPlan = (planSku: string) => {

View File

@ -17,9 +17,11 @@ import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePa
*/ */
export function PublicVpnPlansView() { export function PublicVpnPlansView() {
const servicesBasePath = useServicesBasePath(); const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = usePublicVpnCatalog(); const { data, error } = usePublicVpnCatalog();
const vpnPlans = data?.plans || []; const vpnPlans = data?.plans || [];
const activationFees = data?.activationFees || []; const activationFees = data?.activationFees || [];
// Simple loading check: show skeleton until we have data or an error
const isLoading = !data && !error;
if (isLoading || error) { if (isLoading || error) {
return ( return (

View File

@ -19,8 +19,10 @@ import {
export function SimPlansContainer() { export function SimPlansContainer() {
const router = useRouter(); const router = useRouter();
const servicesBasePath = useServicesBasePath(); const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = useAccountSimCatalog(); const { data, error } = useAccountSimCatalog();
const plans: SimCatalogProduct[] = useMemo(() => data?.plans ?? [], [data?.plans]); const plans: SimCatalogProduct[] = useMemo(() => data?.plans ?? [], [data?.plans]);
// Simple loading check: show skeleton until we have data or an error
const isLoading = !data && !error;
const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice"); const [activeTab, setActiveTab] = useState<SimPlansTab>("data-voice");
const handleSelectPlan = (planSku: string) => { const handleSelectPlan = (planSku: string) => {

View File

@ -13,9 +13,11 @@ import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePa
export function VpnPlansView() { export function VpnPlansView() {
const servicesBasePath = useServicesBasePath(); const servicesBasePath = useServicesBasePath();
const { data, isLoading, error } = useAccountVpnCatalog(); const { data, error } = useAccountVpnCatalog();
const vpnPlans = data?.plans || []; const vpnPlans = data?.plans || [];
const activationFees = data?.activationFees || []; const activationFees = data?.activationFees || [];
// Simple loading check: show skeleton until we have data or an error
const isLoading = !data && !error;
if (isLoading || error) { if (isLoading || error) {
return ( return (

View File

@ -7,7 +7,6 @@ import {
ServerIcon, ServerIcon,
CalendarIcon, CalendarIcon,
DocumentTextIcon, DocumentTextIcon,
GlobeAltIcon,
XCircleIcon, XCircleIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
import { useSubscription } from "@/features/subscriptions/hooks"; import { useSubscription } from "@/features/subscriptions/hooks";
@ -15,6 +14,10 @@ import { InvoicesList } from "@/features/billing/components/InvoiceList/InvoiceL
import { Formatting } from "@customer-portal/domain/toolkit"; import { Formatting } from "@customer-portal/domain/toolkit";
import { PageLayout } from "@/components/templates/PageLayout"; import { PageLayout } from "@/components/templates/PageLayout";
import { StatusPill } from "@/components/atoms/status-pill"; import { StatusPill } from "@/components/atoms/status-pill";
import {
SubscriptionDetailStatsSkeleton,
InvoiceListSkeleton,
} from "@/components/atoms/loading-skeleton";
import { formatIsoDate } from "@/shared/utils"; import { formatIsoDate } from "@/shared/utils";
const { formatCurrency: sharedFormatCurrency } = Formatting; const { formatCurrency: sharedFormatCurrency } = Formatting;
@ -31,7 +34,10 @@ export function SubscriptionDetailContainer() {
const [activeTab, setActiveTab] = useState<"overview" | "sim">("overview"); const [activeTab, setActiveTab] = useState<"overview" | "sim">("overview");
const subscriptionId = parseInt(params.id as string); const subscriptionId = parseInt(params.id as string);
const { data: subscription, isLoading, error } = useSubscription(subscriptionId); const { data: subscription, error } = useSubscription(subscriptionId);
// Simple loading check: show skeleton until we have data or an error
const showLoading = !subscription && !error;
useEffect(() => { useEffect(() => {
const updateTab = () => { const updateTab = () => {
@ -52,12 +58,10 @@ export function SubscriptionDetailContainer() {
const formatCurrency = (amount: number) => sharedFormatCurrency(amount || 0); const formatCurrency = (amount: number) => sharedFormatCurrency(amount || 0);
const pageError = // Show error message (only when we have an error, not during loading)
error || !subscription const pageError = error
? process.env.NODE_ENV === "development" ? process.env.NODE_ENV === "development" && error instanceof Error
? error instanceof Error
? error.message ? error.message
: "Subscription not found"
: "Unable to load subscription details. Please try again." : "Unable to load subscription details. Please try again."
: null; : null;
@ -70,47 +74,66 @@ export function SubscriptionDetailContainer() {
(productNameLower.includes("ntt") && productNameLower.includes("fiber")); (productNameLower.includes("ntt") && productNameLower.includes("fiber"));
const canCancel = subscription?.status === "Active"; const canCancel = subscription?.status === "Active";
// Header action: cancel button (for Internet services)
const headerActions =
isInternetService && canCancel ? (
<Link
href={`/account/subscriptions/${subscriptionId}/cancel`}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-danger-foreground bg-danger hover:bg-danger/90 rounded-lg transition-colors"
>
<XCircleIcon className="h-4 w-4" />
Cancel Service
</Link>
) : undefined;
// Render skeleton matching loading.tsx structure when loading
if (showLoading) {
return (
<PageLayout
icon={<ServerIcon className="h-6 w-6" />}
title="Subscription"
description="Loading subscription details..."
breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" },
{ label: "Subscription" },
]}
>
<div className="space-y-6">
<SubscriptionDetailStatsSkeleton />
<InvoiceListSkeleton rows={5} />
</div>
</PageLayout>
);
}
return ( return (
<PageLayout <PageLayout
icon={<ServerIcon className="h-6 w-6" />} icon={<ServerIcon className="h-6 w-6" />}
title={subscription?.productName ?? "Subscription"} title={subscription?.productName ?? "Subscription"}
description={ actions={headerActions}
subscription ? `Service ID: ${subscription.serviceId}` : "View your subscription details"
}
breadcrumbs={[ breadcrumbs={[
{ label: "Subscriptions", href: "/account/subscriptions" }, { label: "Subscriptions", href: "/account/subscriptions" },
{ label: subscription?.productName ?? "Subscription" }, { label: subscription?.productName ?? "Subscription" },
]} ]}
loading={isLoading}
error={pageError} error={pageError}
> >
{subscription ? ( {subscription ? (
<div className="space-y-6"> <div className="space-y-6">
{/* Main Subscription Card */} {/* Subscription Stats */}
<div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden"> <div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden">
{/* Header with status */} <div className="px-6 py-5 grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="px-6 py-5 border-b border-border">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center">
<ServerIcon className="h-5 w-5 text-primary" />
</div>
<div> <div>
<h2 className="text-lg font-semibold text-foreground">Subscription Details</h2> <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<p className="text-sm text-muted-foreground"> Service Status
Service subscription information </h4>
</p> <div className="mt-2">
</div>
</div>
<StatusPill <StatusPill
label={subscription.status} label={subscription.status}
variant={getSubscriptionStatusVariant(subscription.status)} variant={getSubscriptionStatusVariant(subscription.status)}
size="lg"
/> />
</div> </div>
</div> </div>
{/* Stats Row */}
<div className="px-6 py-5 grid grid-cols-1 md:grid-cols-3 gap-6 bg-muted/20">
<div> <div>
<h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider"> <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
Billing Amount Billing Amount
@ -193,49 +216,6 @@ export function SubscriptionDetailContainer() {
<InvoicesList subscriptionId={subscriptionId} pageSize={5} showFilters={false} /> <InvoicesList subscriptionId={subscriptionId} pageSize={5} showFilters={false} />
</div> </div>
)} )}
{/* Internet Service Actions */}
{isInternetService && activeTab === "overview" && (
<div className="bg-card border border-border rounded-2xl shadow-sm overflow-hidden">
<div className="px-6 py-5 border-b border-border">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-xl bg-primary/10 flex items-center justify-center">
<GlobeAltIcon className="h-5 w-5 text-primary" />
</div>
<div>
<h2 className="text-lg font-semibold text-foreground">Service Actions</h2>
<p className="text-sm text-muted-foreground">
Manage your Internet subscription
</p>
</div>
</div>
</div>
<div className="px-6 py-5">
{canCancel ? (
<div className="flex items-center justify-between">
<div>
<h4 className="text-sm font-medium text-foreground">Cancel Service</h4>
<p className="text-xs text-muted-foreground mt-0.5">
Request cancellation of your Internet subscription
</p>
</div>
<Link
href={`/account/subscriptions/${subscriptionId}/cancel`}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-danger-foreground bg-danger hover:bg-danger/90 rounded-lg transition-colors"
>
<XCircleIcon className="h-4 w-4" />
Request Cancellation
</Link>
</div>
) : (
<p className="text-sm text-muted-foreground">
Service actions are not available for {subscription.status.toLowerCase()}{" "}
subscriptions.
</p>
)}
</div>
</div>
)}
</div> </div>
) : null} ) : null}
</PageLayout> </PageLayout>

View File

@ -5,7 +5,10 @@ import { Button } from "@/components/atoms/button";
import { ErrorBoundary } from "@/components/molecules"; import { ErrorBoundary } from "@/components/molecules";
import { PageLayout } from "@/components/templates/PageLayout"; import { PageLayout } from "@/components/templates/PageLayout";
import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar"; import { SearchFilterBar } from "@/components/molecules/SearchFilterBar/SearchFilterBar";
import { Skeleton } from "@/components/atoms/loading-skeleton"; import {
SubscriptionStatsCardsSkeleton,
SubscriptionTableSkeleton,
} from "@/components/atoms/loading-skeleton";
import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock"; import { AsyncBlock } from "@/components/molecules/AsyncBlock/AsyncBlock";
import { SubscriptionTable } from "@/features/subscriptions/components/SubscriptionTable"; import { SubscriptionTable } from "@/features/subscriptions/components/SubscriptionTable";
import { Server, CheckCircle, XCircle } from "lucide-react"; import { Server, CheckCircle, XCircle } from "lucide-react";
@ -24,11 +27,16 @@ export function SubscriptionsListContainer() {
const { const {
data: subscriptionData, data: subscriptionData,
isLoading,
error, error,
} = useSubscriptions({ status: statusFilter === "all" ? undefined : statusFilter }); isFetching,
} = useSubscriptions({
status: statusFilter === "all" ? undefined : statusFilter,
});
const { data: stats } = useSubscriptionStats(); const { data: stats } = useSubscriptionStats();
// Simple loading check: show skeleton until we have data or an error
const showLoading = !subscriptionData && !error;
const subscriptions = useMemo((): Subscription[] => { const subscriptions = useMemo((): Subscription[] => {
if (!subscriptionData) return []; if (!subscriptionData) return [];
if (Array.isArray(subscriptionData)) return subscriptionData; if (Array.isArray(subscriptionData)) return subscriptionData;
@ -56,7 +64,8 @@ export function SubscriptionsListContainer() {
[] []
); );
if (isLoading || error) { // Show skeleton when loading, error state when error
if (showLoading || error) {
return ( return (
<PageLayout <PageLayout
icon={<Server />} icon={<Server />}
@ -65,47 +74,8 @@ export function SubscriptionsListContainer() {
> >
<AsyncBlock isLoading={false} error={error}> <AsyncBlock isLoading={false} error={error}>
<div className="space-y-6"> <div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> <SubscriptionStatsCardsSkeleton />
{Array.from({ length: 3 }).map((_, i) => ( <SubscriptionTableSkeleton rows={6} />
<div
key={i}
className="bg-card rounded-xl border border-border p-5 shadow-[var(--cp-shadow-1)]"
>
<div className="flex items-center gap-4">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-6 w-12" />
</div>
</div>
</div>
))}
</div>
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">
<div className="px-6 py-4 border-b border-border">
<Skeleton className="h-10 w-full max-w-md rounded-lg" />
</div>
<div className="w-full">
<div className="border-b border-border p-4">
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<Skeleton key={i} className="h-4 w-20" />
))}
</div>
</div>
<div className="divide-y divide-border">
{Array.from({ length: 5 }).map((_, rowIndex) => (
<div key={rowIndex} className="p-4">
<div className="grid grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, colIndex) => (
<Skeleton key={colIndex} className="h-4 w-full" />
))}
</div>
</div>
))}
</div>
</div>
</div>
</div> </div>
</AsyncBlock> </AsyncBlock>
</PageLayout> </PageLayout>
@ -179,7 +149,7 @@ export function SubscriptionsListContainer() {
{/* Subscriptions Table */} {/* Subscriptions Table */}
<SubscriptionTable <SubscriptionTable
subscriptions={filteredSubscriptions} subscriptions={filteredSubscriptions}
loading={isLoading} loading={isFetching && !!subscriptionData}
className="border-0 rounded-none shadow-none" className="border-0 rounded-none shadow-none"
/> />
</div> </div>