@accounter/client
Version:
Accounter client application
199 lines (185 loc) • 6.73 kB
text/typescript
/**
* Pure helpers that turn the `allBusinesses` query nodes into flat rows for the
* business management table. Kept free of React/urql imports so they are easy to unit test.
*/
import { countryNameByCode } from '../../helpers/countries.js';
export type BusinessRow = {
id: string;
name: string;
hebrewName: string | null;
governmentId: string | null;
countryCode: string | null;
city: string | null;
zipCode: string | null;
createdAt: Date | null;
updatedAt: Date | null;
// categorization
sortCodeKey: number | null;
sortCodeName: string | null;
taxCategoryName: string | null;
irsCode: number | null;
pcn874RecordType: string | null;
// extension tags
isClient: boolean;
isAdmin: boolean;
isActive: boolean;
// suggestion defaults
suggestionDescription: string | null;
suggestionTags: string[];
};
/** Usage counts merged into table rows once the lazy usage query resolves (null until loaded). */
export type BusinessUsageCounts = {
totalTransactions: number | null;
totalDocuments: number | null;
totalMiscExpenses: number | null;
totalLedgerRecords: number | null;
};
/** A business table row: the mapped business plus its (optionally loaded) usage counts. */
export type BusinessTableRow = BusinessRow & BusinessUsageCounts;
/** Extra handles passed to the table via `table.options.meta` and read by cells. */
export type BusinessTableMeta = {
usageFetching?: boolean;
refetchBusinesses?: () => void;
};
/** True once all four usage counts are known (loaded) and zero — the delete guard. */
export function isBusinessUnused(row: BusinessUsageCounts): boolean {
return (
row.totalTransactions === 0 &&
row.totalDocuments === 0 &&
row.totalMiscExpenses === 0 &&
row.totalLedgerRecords === 0
);
}
/** Minimal shape of an `allBusinesses` node consumed by the table (LtdFinancialEntity fields). */
type RawBusinessNode = {
__typename?: string | null;
id: string;
name: string;
hebrewName?: string | null;
governmentId?: string | null;
country?: { code?: string | null } | null;
city?: string | null;
zipCode?: string | null;
// DateTime scalars arrive as ISO strings at runtime (no urql scalar exchange), even though
// codegen types them as Date; accept both and parse below.
createdAt?: string | Date | null;
updatedAt?: string | Date | null;
sortCode?: { key?: number | null; name?: string | null } | null;
taxCategory?: { name?: string | null } | null;
irsCode?: number | null;
pcn874RecordType?: string | null;
isClient?: boolean | null;
isAdmin?: boolean | null;
isActive?: boolean | null;
suggestions?: {
description?: string | null;
tags?: ReadonlyArray<{ name?: string | null }> | null;
} | null;
};
/** Parse an ISO string / Date into a Date, returning null for missing or malformed values. */
function parseValidDate(value: string | Date | null | undefined): Date | null {
if (!value) {
return null;
}
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
/** Keep only company businesses, map them to table rows and sort by name (case-insensitive). */
export function businessNodesToRows(nodes: readonly RawBusinessNode[]): BusinessRow[] {
return nodes
.filter(node => node.__typename === 'LtdFinancialEntity')
.map(node => ({
id: node.id,
name: node.name,
hebrewName: node.hebrewName ?? null,
governmentId: node.governmentId ?? null,
countryCode: node.country?.code ?? null,
city: node.city ?? null,
zipCode: node.zipCode ?? null,
createdAt: parseValidDate(node.createdAt),
updatedAt: parseValidDate(node.updatedAt),
sortCodeKey: node.sortCode?.key ?? null,
sortCodeName: node.sortCode?.name ?? null,
taxCategoryName: node.taxCategory?.name ?? null,
irsCode: node.irsCode ?? null,
pcn874RecordType: node.pcn874RecordType ?? null,
isClient: node.isClient ?? false,
isAdmin: node.isAdmin ?? false,
isActive: node.isActive ?? true,
suggestionDescription: node.suggestions?.description ?? null,
suggestionTags:
node.suggestions?.tags?.map(tag => tag.name).filter((name): name is string => !!name) ?? [],
}))
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
}
/** Locality shown in the table: the country name resolved from the stored country code. */
export function formatLocality(row: Pick<BusinessRow, 'countryCode'>): string {
return countryNameByCode(row.countryCode) ?? '';
}
/** Merge per-business usage counts (from the lazy usage query) into the table rows by id. */
export function mergeBusinessUsage(
rows: readonly BusinessRow[],
usage: readonly ({ businessId: string } & BusinessUsageCounts)[],
): BusinessTableRow[] {
const byId = new Map<string, BusinessUsageCounts>();
for (const entry of usage) {
byId.set(entry.businessId, entry);
}
return rows.map(row => {
const counts = byId.get(row.id);
return {
...row,
totalTransactions: counts?.totalTransactions ?? null,
totalDocuments: counts?.totalDocuments ?? null,
totalMiscExpenses: counts?.totalMiscExpenses ?? null,
totalLedgerRecords: counts?.totalLedgerRecords ?? null,
};
});
}
/** Client-side filters applied over all business rows. */
export type BusinessRowFilters = {
name: string;
client: boolean;
admin: boolean;
inactive: boolean;
unusedOnly: boolean;
sortCode: string;
taxCategory: string;
};
/** Filter rows by the extension-tag flags, usage and free-text sort-code / tax-category. */
export function filterBusinessRows(
rows: readonly BusinessTableRow[],
filters: BusinessRowFilters,
): BusinessTableRow[] {
const name = filters.name.trim().toLowerCase();
const sortCode = filters.sortCode.trim().toLowerCase();
const taxCategory = filters.taxCategory.trim().toLowerCase();
return rows.filter(row => {
if (name && !`${row.name} ${row.hebrewName ?? ''}`.toLowerCase().includes(name)) {
return false;
}
if (filters.client && !row.isClient) {
return false;
}
if (filters.admin && !row.isAdmin) {
return false;
}
if (filters.inactive && row.isActive) {
return false;
}
if (filters.unusedOnly && !isBusinessUnused(row)) {
return false;
}
if (sortCode) {
const keyMatch = String(row.sortCodeKey ?? '').includes(sortCode);
const nameMatch = (row.sortCodeName ?? '').toLowerCase().includes(sortCode);
if (!keyMatch && !nameMatch) {
return false;
}
}
if (taxCategory && !(row.taxCategoryName ?? '').toLowerCase().includes(taxCategory)) {
return false;
}
return true;
});
}