All files / lib/customer-query customer-query.ts

100% Statements 21/21
100% Branches 21/21
100% Functions 8/8
100% Lines 20/20

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              27x 27x 27x 27x     27x   540x         540x 540x   540x     27x 27x 27x 27x 411x     27x             411x 411x         411x     392x 411x        
import type {Customer, CustomerFilters, CustomersResponse} from "@/types/dashboard";
 
/** Apply customer search, filters, pagination, and filtered-set summary metrics. */
export function queryCustomers(
    customers: readonly Customer[],
    filters: CustomerFilters,
): CustomersResponse {
  const pageSize = filters.pageSize ?? 8;
  const query = filters.query?.trim().toLowerCase() ?? "";
  const status = filters.status ?? "all";
  const plan = filters.plan ?? "all";
 
  // Search intentionally spans business fields that operators scan most often.
  const filtered = customers.filter((customer) => {
    const matchesQuery =
        query.length === 0 ||
        [customer.company, customer.owner, customer.region]
            .join(" ")
            .toLowerCase()
            .includes(query);
    const matchesStatus = status === "all" || customer.status === status;
    const matchesPlan = plan === "all" || customer.plan === plan;
 
    return matchesQuery && matchesStatus && matchesPlan;
  });
 
  const totalPages = Math.max(Math.ceil(filtered.length / pageSize), 1);
  const page = Math.min(Math.max(filters.page ?? 1, 1), totalPages);
  const start = (page - 1) * pageSize;
  const data = filtered.slice(start, start + pageSize);
  const mrrCustomers = filtered.filter((customer) => customer.status !== "churned");
 
  // Summary values are derived from the filtered set so side panels match the table.
  return {
    data,
    page,
    pageSize,
    total: filtered.length,
    totalPages,
    summary: {
      active: filtered.filter((customer) => customer.status === "active").length,
      atRisk: filtered.filter((customer) => customer.status === "past_due").length,
      averageHealth:
          filtered.length === 0
              ? 0
              : Math.round(
                  filtered.reduce((total, customer) => total + customer.healthScore, 0) /
                  filtered.length,
              ),
      totalMrr: mrrCustomers.reduce((total, customer) => total + customer.mrr, 0),
      trial: filtered.filter((customer) => customer.status === "trial").length,
    },
  };
}