feat(companies): show member + yacht counts on list page
listCompanies returns memberCount (active companyMemberships) and yachtCount (yachts where currentOwnerType=company), each fetched as a parallel grouped count after the main page query. Two new badge columns in company-columns render them between the tax-id and status columns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import Link from 'next/link';
|
|||||||
import { MoreHorizontal, Pencil, Archive, Eye } from 'lucide-react';
|
import { MoreHorizontal, Pencil, Archive, Eye } from 'lucide-react';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -12,7 +13,6 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
|
||||||
// TODO: add member/yacht counts once the list endpoint returns them via a join.
|
|
||||||
export interface CompanyRow {
|
export interface CompanyRow {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -27,6 +27,8 @@ export interface CompanyRow {
|
|||||||
archivedAt: string | null;
|
archivedAt: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
memberCount?: number;
|
||||||
|
yachtCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
@@ -88,6 +90,28 @@ export function getCompanyColumns({
|
|||||||
return <span className="text-sm">{value}</span>;
|
return <span className="text-sm">{value}</span>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'memberCount',
|
||||||
|
header: 'Members',
|
||||||
|
enableSorting: false,
|
||||||
|
size: 88,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const n = row.original.memberCount ?? 0;
|
||||||
|
if (n === 0) return <span className="text-muted-foreground">—</span>;
|
||||||
|
return <Badge variant="secondary">{n}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'yachtCount',
|
||||||
|
header: 'Yachts',
|
||||||
|
enableSorting: false,
|
||||||
|
size: 88,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const n = row.original.yachtCount ?? 0;
|
||||||
|
if (n === 0) return <span className="text-muted-foreground">—</span>;
|
||||||
|
return <Badge variant="secondary">{n}</Badge>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'status',
|
id: 'status',
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { and, eq, ilike, or, sql } from 'drizzle-orm';
|
import { and, count, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { companies, companyTags } from '@/lib/db/schema/companies';
|
import { companies, companyMemberships, companyTags } from '@/lib/db/schema/companies';
|
||||||
import type { Company } from '@/lib/db/schema/companies';
|
import type { Company } from '@/lib/db/schema/companies';
|
||||||
|
import { yachts } from '@/lib/db/schema/yachts';
|
||||||
import { withTransaction } from '@/lib/db/utils';
|
import { withTransaction } from '@/lib/db/utils';
|
||||||
import { buildListQuery } from '@/lib/db/query-builder';
|
import { buildListQuery } from '@/lib/db/query-builder';
|
||||||
import { createAuditLog } from '@/lib/audit';
|
import { createAuditLog } from '@/lib/audit';
|
||||||
@@ -238,7 +239,41 @@ export async function listCompanies(portId: string, query: ListCompaniesInput) {
|
|||||||
archivedAtColumn: companies.archivedAt,
|
archivedAtColumn: companies.archivedAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
if (result.data.length === 0) return result;
|
||||||
|
|
||||||
|
const ids = result.data.map((r) => r.id);
|
||||||
|
|
||||||
|
const [memberCounts, yachtCounts] = await Promise.all([
|
||||||
|
db
|
||||||
|
.select({ companyId: companyMemberships.companyId, count: count() })
|
||||||
|
.from(companyMemberships)
|
||||||
|
.where(and(inArray(companyMemberships.companyId, ids), isNull(companyMemberships.endDate)))
|
||||||
|
.groupBy(companyMemberships.companyId),
|
||||||
|
db
|
||||||
|
.select({ ownerId: yachts.currentOwnerId, count: count() })
|
||||||
|
.from(yachts)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(yachts.portId, portId),
|
||||||
|
eq(yachts.currentOwnerType, 'company'),
|
||||||
|
inArray(yachts.currentOwnerId, ids),
|
||||||
|
isNull(yachts.archivedAt),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.groupBy(yachts.currentOwnerId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const memberCountMap = new Map(memberCounts.map((r) => [r.companyId, r.count]));
|
||||||
|
const yachtCountMap = new Map(yachtCounts.map((r) => [r.ownerId, r.count]));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: result.data.map((row) => ({
|
||||||
|
...row,
|
||||||
|
memberCount: memberCountMap.get(row.id) ?? 0,
|
||||||
|
yachtCount: yachtCountMap.get(row.id) ?? 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Autocomplete ────────────────────────────────────────────────────────────
|
// ─── Autocomplete ────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user