ad0855902b
Phase 1 — Quick Wins: centralize formatMoney/formatCents, extract findUniqueOrThrow helper (19 routers), shared Prisma select constants, useInvalidatePlanningViews hook, status badge consolidation, composite DB indexes. Phase 2 — Timeline Split: extract TimelineContext, TimelineResourcePanel, TimelineProjectPanel; split 28-dep useMemo into 3 focused memos. TimelineView.tsx reduced from 1,903 to 538 lines. Phase 3 — Query Performance: server-side filtering for getEntriesView, remove availability from timeline resource select, SSE event debouncing (50ms batch window). Phase 4 — Estimate Workspace: extract 7 tab components and 3 editor components. EstimateWorkspaceClient 1,298→306 lines, EstimateWorkspaceDraftEditor 1,205→581 lines. Phase 5 — Package Cleanup: split commit-dispo-import-batch (1,112→573 lines), extract shared pagination helper with 11 tests. All tests pass: 209 API, 254 engine, 67 application. Co-Authored-By: claude-flow <ruv@ruv.net>
223 lines
8.3 KiB
TypeScript
223 lines
8.3 KiB
TypeScript
import type { WeekdayAvailability } from "@planarchy/shared";
|
|
import {
|
|
createWeekdayAvailabilityFromFte,
|
|
normalizeDispoRoleToken,
|
|
} from "@planarchy/shared";
|
|
import type { TxClient, MergedStagedResource } from "./commit-dispo-batch-types.js";
|
|
import { deriveRoleTokens } from "./shared.js";
|
|
|
|
function asNullableString(value: unknown): string | null {
|
|
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
}
|
|
|
|
function normalizeFallbackRoleName(value: string): string {
|
|
return value.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function isFiniteNumber(value: unknown): value is number {
|
|
return typeof value === "number" && Number.isFinite(value);
|
|
}
|
|
|
|
function asObject(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === "object" && !Array.isArray(value)
|
|
? (value as Record<string, unknown>)
|
|
: {};
|
|
}
|
|
|
|
function mergeScalar<T>(
|
|
current: T | null,
|
|
incoming: T | null | undefined,
|
|
): T | null {
|
|
return incoming ?? current;
|
|
}
|
|
|
|
export function inferRoleNameFromResource(resource: MergedStagedResource): string | null {
|
|
const explicitRoleName = Array.from(resource.roleTokens)
|
|
.map((token) => normalizeDispoRoleToken(token))
|
|
.find((roleName): roleName is string => Boolean(roleName));
|
|
if (explicitRoleName) {
|
|
return explicitRoleName;
|
|
}
|
|
|
|
const derivedRoleName = deriveRoleTokens(
|
|
resource.chapter,
|
|
asNullableString(resource.rawPayload.department),
|
|
asNullableString(resource.rawPayload.mainSkillset),
|
|
asNullableString(resource.rawPayload.sapOrgUnitLevelSix),
|
|
asNullableString(resource.rawPayload.sapOrgUnitLevelSeven),
|
|
asNullableString(resource.rawPayload.sapEmployeeName),
|
|
)
|
|
.map((token) => normalizeDispoRoleToken(token))
|
|
.find((roleName): roleName is string => Boolean(roleName));
|
|
if (derivedRoleName) {
|
|
return derivedRoleName;
|
|
}
|
|
|
|
if (resource.chapter === "Art Direction") {
|
|
return "Art Director";
|
|
}
|
|
if (resource.chapter === "Project Management") {
|
|
return "Project Manager";
|
|
}
|
|
|
|
const fallbackRoleLabel =
|
|
asNullableString(resource.rawPayload.department) ??
|
|
asNullableString(resource.rawPayload.mainSkillset) ??
|
|
asNullableString(resource.chapter) ??
|
|
asNullableString(resource.rawPayload.sapOrgUnitLevelSeven) ??
|
|
asNullableString(resource.rawPayload.sapOrgUnitLevelSix);
|
|
|
|
return fallbackRoleLabel ? normalizeFallbackRoleName(fallbackRoleLabel) : null;
|
|
}
|
|
|
|
export function mergeStagedResources(
|
|
rows: Awaited<ReturnType<TxClient["stagedResource"]["findMany"]>>,
|
|
): Map<string, MergedStagedResource> {
|
|
const sourcePriority = new Map([
|
|
["CHARGEABILITY", 1],
|
|
["ROSTER", 2],
|
|
]);
|
|
const ordered = [...rows].sort(
|
|
(left, right) =>
|
|
(sourcePriority.get(left.sourceKind) ?? 0) - (sourcePriority.get(right.sourceKind) ?? 0),
|
|
);
|
|
const merged = new Map<string, MergedStagedResource>();
|
|
|
|
for (const row of ordered) {
|
|
const existing = merged.get(row.canonicalExternalId);
|
|
const rawPayload = asObject(row.rawPayload);
|
|
|
|
if (!existing) {
|
|
merged.set(row.canonicalExternalId, {
|
|
availability: row.availability ?? null,
|
|
canonicalExternalId: row.canonicalExternalId,
|
|
chapter: row.chapter ?? null,
|
|
chargeabilityTarget: row.chargeabilityTarget ?? null,
|
|
clientUnitName: row.clientUnitName ?? null,
|
|
countryCode: row.countryCode ?? null,
|
|
displayName: row.displayName ?? null,
|
|
email: row.email ?? null,
|
|
fte: row.fte ?? null,
|
|
lcrCents: row.lcrCents ?? null,
|
|
managementLevelGroupName: row.managementLevelGroupName ?? null,
|
|
managementLevelName: row.managementLevelName ?? null,
|
|
metroCityName: row.metroCityName ?? null,
|
|
rawPayload,
|
|
resourceType: row.resourceType ?? null,
|
|
roleTokens: new Set(row.roleTokens),
|
|
sourceKinds: [row.sourceKind],
|
|
ucrCents: row.ucrCents ?? null,
|
|
vacationDaysPerYear: isFiniteNumber(rawPayload.vacationDaysPerYear)
|
|
? rawPayload.vacationDaysPerYear
|
|
: null,
|
|
warnings: [...row.warnings],
|
|
});
|
|
continue;
|
|
}
|
|
|
|
existing.availability = mergeScalar(existing.availability, row.availability);
|
|
existing.chapter = mergeScalar(existing.chapter, row.chapter);
|
|
existing.chargeabilityTarget = mergeScalar(existing.chargeabilityTarget, row.chargeabilityTarget);
|
|
existing.clientUnitName = mergeScalar(existing.clientUnitName, row.clientUnitName);
|
|
existing.countryCode = mergeScalar(existing.countryCode, row.countryCode);
|
|
existing.displayName = mergeScalar(existing.displayName, row.displayName);
|
|
existing.email = mergeScalar(existing.email, row.email);
|
|
existing.fte = mergeScalar(existing.fte, row.fte);
|
|
existing.lcrCents = mergeScalar(existing.lcrCents, row.lcrCents);
|
|
existing.managementLevelGroupName = mergeScalar(
|
|
existing.managementLevelGroupName,
|
|
row.managementLevelGroupName,
|
|
);
|
|
existing.managementLevelName = mergeScalar(existing.managementLevelName, row.managementLevelName);
|
|
existing.metroCityName = mergeScalar(existing.metroCityName, row.metroCityName);
|
|
existing.resourceType = mergeScalar(existing.resourceType, row.resourceType);
|
|
existing.ucrCents = mergeScalar(existing.ucrCents, row.ucrCents);
|
|
if (existing.availability === null && row.availability !== null) {
|
|
existing.availability = row.availability;
|
|
}
|
|
for (const roleToken of row.roleTokens) {
|
|
existing.roleTokens.add(roleToken);
|
|
}
|
|
existing.sourceKinds.push(row.sourceKind);
|
|
existing.warnings.push(...row.warnings);
|
|
existing.rawPayload = {
|
|
...existing.rawPayload,
|
|
...rawPayload,
|
|
};
|
|
if (isFiniteNumber(rawPayload.vacationDaysPerYear)) {
|
|
existing.vacationDaysPerYear = rawPayload.vacationDaysPerYear;
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
export function parseWeekdayAvailability(
|
|
value: unknown,
|
|
fallbackFte: number | null,
|
|
): WeekdayAvailability {
|
|
const fallback = createWeekdayAvailabilityFromFte(fallbackFte ?? 1);
|
|
const source = asObject(value);
|
|
|
|
return {
|
|
monday: isFiniteNumber(source.monday) ? source.monday : fallback.monday,
|
|
tuesday: isFiniteNumber(source.tuesday) ? source.tuesday : fallback.tuesday,
|
|
wednesday: isFiniteNumber(source.wednesday) ? source.wednesday : fallback.wednesday,
|
|
thursday: isFiniteNumber(source.thursday) ? source.thursday : fallback.thursday,
|
|
friday: isFiniteNumber(source.friday) ? source.friday : fallback.friday,
|
|
};
|
|
}
|
|
|
|
export interface ReferenceDataMaps {
|
|
clientIdByCode: Map<string, string>;
|
|
clientIdByName: Map<string, string>;
|
|
countryIdByCode: Map<string, string>;
|
|
managementLevelGroupByName: Map<string, { id: string; name: string; targetPercentage: number }>;
|
|
managementLevelIdByName: Map<string, string>;
|
|
metroCityIdByName: Map<string, string>;
|
|
orgUnitIdByLevelAndName: Map<string, string>;
|
|
roleIdByName: Map<string, string>;
|
|
utilizationCategoryIdByCode: Map<string, string>;
|
|
}
|
|
|
|
export function buildReferenceDataMaps(data: {
|
|
clients: { id: string; code: string | null; name: string }[];
|
|
countries: { id: string; code: string }[];
|
|
managementLevelGroups: { id: string; name: string; targetPercentage: number }[];
|
|
managementLevels: { id: string; name: string }[];
|
|
metroCities: { id: string; name: string }[];
|
|
orgUnits: { id: string; level: number; name: string }[];
|
|
roles: { id: string; name: string }[];
|
|
utilizationCategories: { id: string; code: string }[];
|
|
}): ReferenceDataMaps {
|
|
return {
|
|
clientIdByCode: new Map(
|
|
data.clients.filter((client) => client.code).map((client) => [client.code!, client.id]),
|
|
),
|
|
clientIdByName: new Map(
|
|
data.clients.map((client) => [client.name.toLowerCase(), client.id]),
|
|
),
|
|
countryIdByCode: new Map(
|
|
data.countries.map((country) => [country.code, country.id]),
|
|
),
|
|
managementLevelGroupByName: new Map(
|
|
data.managementLevelGroups.map((group) => [group.name, group]),
|
|
),
|
|
managementLevelIdByName: new Map(
|
|
data.managementLevels.map((level) => [level.name, level.id]),
|
|
),
|
|
metroCityIdByName: new Map(
|
|
data.metroCities.map((metroCity) => [metroCity.name.toLowerCase(), metroCity.id]),
|
|
),
|
|
orgUnitIdByLevelAndName: new Map(
|
|
data.orgUnits.map((orgUnit) => [`${orgUnit.level}:${orgUnit.name.toLowerCase()}`, orgUnit.id]),
|
|
),
|
|
roleIdByName: new Map(
|
|
data.roles.map((role) => [role.name, role.id]),
|
|
),
|
|
utilizationCategoryIdByCode: new Map(
|
|
data.utilizationCategories.map((category) => [category.code, category.id]),
|
|
),
|
|
};
|
|
}
|