All files / components/dashboard/customers-page-client customers-page-client.tsx

94.73% Statements 18/19
55.55% Branches 10/18
90% Functions 9/10
94.44% Lines 17/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 1301x                         1x                 7x 2x     8x           5x                                                     7x     20x                             7x               7x 3x                   7x   4x       7x 1x     7x                                 1x            
"use client";
 
import {keepPreviousData, useQuery} from "@tanstack/react-query";
import {CreditCard, ShieldAlert, Sparkles, UsersRound} from "lucide-react";
import {useMemo, useState} from "react";
import {CustomerTable} from "../customer-table/customer-table";
import {DashboardShell} from "@/components/dashboard/dashboard-shell/dashboard-shell";
import {Skeleton} from "@/components/ui/skeleton/skeleton";
import {uiStyles} from "@/components/ui/style-primitives";
import {fetchCustomers} from "@/lib/api/api";
import {cn, formatCurrency, formatNumber} from "@/lib/utils/utils";
import type {CustomerFilters, CustomersResponse} from "@/types/dashboard";
 
const pageSize = 8;
 
function CustomerSummary({
                           data,
                           isLoading,
                         }: {
  data?: CustomersResponse;
  isLoading: boolean;
}) {
  if (isLoading) {
    return (
        <section aria-label="Customer summary" className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
          {Array.from({length: 4}).map((_, index) => (
              <Skeleton className="h-28" key={index}/>
          ))}
        </section>
    );
  }
 
  const cards = [
    {
      caption: "Accounts in the current segment",
      icon: UsersRound,
      label: "Total customers",
      value: formatNumber(data?.total ?? 0),
    },
    {
      caption: "Recurring revenue in scope",
      icon: CreditCard,
      label: "Filtered MRR",
      value: formatCurrency(data?.summary.totalMrr ?? 0, true),
    },
    {
      caption: "Past due customers needing follow-up",
      icon: ShieldAlert,
      label: "At risk",
      value: formatNumber(data?.summary.atRisk ?? 0),
    },
    {
      caption: "Trials ready for conversion review",
      icon: Sparkles,
      label: "Trials",
      value: formatNumber(data?.summary.trial ?? 0),
    },
  ];
 
  return (
      <section aria-label="Customer summary" className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
        {cards.map((card) => (
            <article className={cn("p-4", uiStyles.surface)} key={card.label}>
              <p className="flex items-center gap-2 text-sm text-slate-500 dark:text-slate-400">
                <card.icon aria-hidden="true" size={16}/>
                {card.label}
              </p>
              <p className="mt-3 text-2xl font-semibold text-slate-950 dark:text-white">{card.value}</p>
              <p className="mt-2 text-sm text-slate-500 dark:text-slate-400">{card.caption}</p>
            </article>
        ))}
      </section>
  );
}
 
/** Focused customer route with segmentation, paging, and account health context. */
export function CustomersPageClient() {
  const [filters, setFilters] = useState<CustomerFilters>({
    page: 1,
    pageSize,
    plan: "all",
    query: "",
    status: "all",
  });
 
  const customerFilters = useMemo(
      () => ({
        page: filters.page ?? 1,
        pageSize,
        plan: filters.plan ?? "all",
        query: filters.query ?? "",
        status: filters.status ?? "all",
      }),
      [filters.page, filters.plan, filters.query, filters.status],
  );
 
  const customersQuery = useQuery({
    placeholderData: keepPreviousData,
    queryFn: () => fetchCustomers(customerFilters),
    queryKey: ["customers", customerFilters],
  });
 
  const updateFilters = (nextFilters: Partial<CustomerFilters>) => {
    setFilters((current) => ({...current, ...nextFilters}));
  };
 
  return (
      <DashboardShell
          activeSection="customers"
          description="Segment customers, monitor risk, and review account-level health without crowding the executive overview."
          eyebrow="Customers"
          isRefreshing={customersQuery.isFetching}
          onRefresh={() => void customersQuery.refetch()}
          title="Customers"
      >
        <CustomerSummary data={customersQuery.data} isLoading={customersQuery.isLoading}/>
        <CustomerTable
            data={customersQuery.data}
            filters={customerFilters}
            isError={customersQuery.isError}
            isFetching={customersQuery.isFetching}
            isLoading={customersQuery.isLoading}
            onChangeFilters={updateFilters}
            onRetry={() => void customersQuery.refetch()}
        />
      </DashboardShell>
  );
}