cd78f72f33
Complete rename of all technical identifiers across the codebase: Package names (11 packages): - @planarchy/* → @capakraken/* in all package.json, tsconfig, imports Import statements: 277 files, 548 occurrences replaced Database & Docker: - PostgreSQL user/db: planarchy → capakraken - Docker volumes: planarchy_pgdata → capakraken_pgdata - Connection strings updated in docker-compose, .env, CI CI/CD: - GitHub Actions workflow: all filter commands updated - Test database credentials updated Infrastructure: - Redis channel: planarchy:sse → capakraken:sse - Logger service name: planarchy-api → capakraken-api - Anonymization seed updated - Start/stop/restart scripts updated Test data: - Seed emails: @planarchy.dev → @capakraken.dev - E2E test credentials: all 11 spec files updated - Email defaults: @planarchy.app → @capakraken.app - localStorage keys: planarchy_* → capakraken_* Documentation: 30+ .md files updated Verification: - pnpm install: workspace resolution works - TypeScript: only pre-existing TS2589 (no new errors) - Engine: 310/310 tests pass - Staffing: 37/37 tests pass Co-Authored-By: claude-flow <ruv@ruv.net>
656 lines
17 KiB
TypeScript
656 lines
17 KiB
TypeScript
import path from "node:path";
|
|
import type { Prisma, PrismaClient } from "@capakraken/db";
|
|
import {
|
|
DispoImportSourceKind,
|
|
DispoStagedRecordType,
|
|
ImportBatchStatus,
|
|
ResourceType,
|
|
StagedRecordStatus,
|
|
} from "@capakraken/db";
|
|
import {
|
|
createWeekdayAvailabilityFromFte,
|
|
normalizeCanonicalResourceIdentity,
|
|
normalizeDispoChapterToken,
|
|
} from "@capakraken/shared";
|
|
|
|
export type DispoImportDbClient = Pick<
|
|
PrismaClient,
|
|
| "client"
|
|
| "country"
|
|
| "importBatch"
|
|
| "managementLevel"
|
|
| "managementLevelGroup"
|
|
| "metroCity"
|
|
| "orgUnit"
|
|
| "stagedAssignment"
|
|
| "stagedAvailabilityRule"
|
|
| "stagedClient"
|
|
| "stagedProject"
|
|
| "stagedResource"
|
|
| "stagedVacation"
|
|
| "stagedUnresolvedRecord"
|
|
>;
|
|
|
|
export interface DispoReferenceImportInput {
|
|
importBatchId?: string;
|
|
notes?: string | null;
|
|
referenceWorkbookPath: string;
|
|
}
|
|
|
|
export interface DispoChargeabilityImportInput {
|
|
chargeabilityWorkbookPath: string;
|
|
excludedResourceExternalIds?: string[];
|
|
importBatchId?: string;
|
|
notes?: string | null;
|
|
}
|
|
|
|
export interface DispoRosterImportInput {
|
|
costWorkbookPath?: string;
|
|
importBatchId?: string;
|
|
notes?: string | null;
|
|
rosterWorkbookPath: string;
|
|
}
|
|
|
|
export interface DispoPlanningImportInput {
|
|
excludedResourceExternalIds?: string[];
|
|
importBatchId?: string;
|
|
notes?: string | null;
|
|
planningWorkbookPath: string;
|
|
}
|
|
|
|
export interface ParsedCountryReference {
|
|
sourceRow: number;
|
|
countryCode: string;
|
|
name: string;
|
|
dailyWorkingHours: number;
|
|
metroCities: string[];
|
|
scheduleRules?: Prisma.InputJsonValue;
|
|
}
|
|
|
|
export interface ParsedOrgUnitReference {
|
|
sourceRow: number;
|
|
level: number;
|
|
name: string;
|
|
parentName: string | null;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export interface ParsedManagementLevelGroupReference {
|
|
sourceRow: number;
|
|
name: string;
|
|
targetPercentage: number;
|
|
sortOrder: number;
|
|
levels: string[];
|
|
}
|
|
|
|
export interface ParsedClientReference {
|
|
sourceColumn: string;
|
|
sourceRow: number;
|
|
clientCode: string | null;
|
|
name: string;
|
|
parentClientCode: string | null;
|
|
parentName: string | null;
|
|
sortOrder: number;
|
|
}
|
|
|
|
export interface ParsedReferenceWorkbook {
|
|
clients: ParsedClientReference[];
|
|
countries: ParsedCountryReference[];
|
|
managementLevelGroups: ParsedManagementLevelGroupReference[];
|
|
orgUnits: ParsedOrgUnitReference[];
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedChargeabilityResource {
|
|
availability: Prisma.InputJsonValue;
|
|
canonicalExternalId: string;
|
|
chapter: string | null;
|
|
chapterCode: string | null;
|
|
chargeabilityTarget: number | null;
|
|
clientUnitName: string | null;
|
|
countryCode: string | null;
|
|
displayName: string;
|
|
eid: string;
|
|
email: string | null;
|
|
enterpriseId: string;
|
|
fte: number | null;
|
|
managementLevelGroupName: string | null;
|
|
managementLevelName: string | null;
|
|
metroCityName: string | null;
|
|
rawResourceType: string | null;
|
|
resourceType: ResourceType;
|
|
roleTokens: string[];
|
|
sourceRow: number;
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedUnresolvedRecord {
|
|
message: string;
|
|
normalizedData: Record<string, unknown>;
|
|
projectKey?: string | null;
|
|
recordType: DispoStagedRecordType;
|
|
resolutionHint?: string | null;
|
|
resourceExternalId?: string | null;
|
|
sourceColumn?: string | null;
|
|
sourceRow: number;
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedChargeabilityWorkbook {
|
|
resources: ParsedChargeabilityResource[];
|
|
unresolved: ParsedUnresolvedRecord[];
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedRosterResource {
|
|
availability: Prisma.InputJsonValue;
|
|
canonicalExternalId: string;
|
|
chapter: string | null;
|
|
chapterCode: string | null;
|
|
clientUnitName: string | null;
|
|
countryCode: string | null;
|
|
dailyWorkingHoursPerFte: number | null;
|
|
department: string | null;
|
|
displayName: string;
|
|
eid: string;
|
|
email: string | null;
|
|
enterpriseId: string;
|
|
firstDayInDispo: Date | null;
|
|
fte: number | null;
|
|
lastDayInDispo: Date | null;
|
|
lcrCents: number | null;
|
|
mainSkillset: string | null;
|
|
managementLevelGroupName: string | null;
|
|
managementLevelName: string | null;
|
|
metroCityName: string | null;
|
|
rawResourceType: string | null;
|
|
resourceHoursPerWeek: number | null;
|
|
resourceType: ResourceType;
|
|
rateResolution: "EXACT" | "LEVEL_AVERAGE" | "MISSING";
|
|
rateResolutionLevel: string | null;
|
|
roleTokens: string[];
|
|
sapEmployeeName: string | null;
|
|
sapOrgUnitLevelFive: string | null;
|
|
sapOrgUnitLevelSix: string | null;
|
|
sapOrgUnitLevelSeven: string | null;
|
|
sourceRow: number;
|
|
sourceSheet: string;
|
|
ucrCents: number | null;
|
|
vacationDaysPerYear: number | null;
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedRosterWorkbook {
|
|
excludedCanonicalExternalIds: string[];
|
|
ignoredPseudoDemandRows: number;
|
|
resources: ParsedRosterResource[];
|
|
unresolved: ParsedUnresolvedRecord[];
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedPlanningAssignment {
|
|
assignmentDate: Date;
|
|
chapterToken: string | null;
|
|
hoursPerDay: number;
|
|
isInternal: boolean;
|
|
isTbd: boolean;
|
|
isUnassigned: boolean;
|
|
percentage: number;
|
|
projectKey: string | null;
|
|
rawToken: string;
|
|
resourceExternalId: string;
|
|
roleName: string | null;
|
|
roleToken: string | null;
|
|
slotFraction: number;
|
|
sourceColumn: string;
|
|
sourceRow: number;
|
|
utilizationCategoryCode: string | null;
|
|
warnings: string[];
|
|
winProbability: number | null;
|
|
}
|
|
|
|
export interface ParsedPlanningVacation {
|
|
endDate: Date;
|
|
halfDayPart: string | null;
|
|
holidayName: string | null;
|
|
isHalfDay: boolean;
|
|
isPublicHoliday: boolean;
|
|
note: string | null;
|
|
rawToken: string;
|
|
resourceExternalId: string;
|
|
sourceColumn: string;
|
|
sourceRow: number;
|
|
startDate: Date;
|
|
vacationType: "ANNUAL" | "OTHER" | "PUBLIC_HOLIDAY" | "SICK";
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedPlanningAvailabilityRule {
|
|
availableHours: number | null;
|
|
effectiveEndDate: Date | null;
|
|
effectiveStartDate: Date | null;
|
|
isResolved: boolean;
|
|
percentage: number | null;
|
|
rawToken: string;
|
|
resourceExternalId: string;
|
|
ruleType: string;
|
|
sourceColumn: string;
|
|
sourceRow: number;
|
|
warnings: string[];
|
|
}
|
|
|
|
export interface ParsedPlanningWorkbook {
|
|
assignments: ParsedPlanningAssignment[];
|
|
availabilityRules: ParsedPlanningAvailabilityRule[];
|
|
unresolved: ParsedUnresolvedRecord[];
|
|
vacations: ParsedPlanningVacation[];
|
|
warnings: string[];
|
|
}
|
|
|
|
const COUNTRY_REFERENCE_CONFIG = {
|
|
"Costa Rica": {
|
|
code: "CR",
|
|
dailyWorkingHours: 8,
|
|
},
|
|
Germany: {
|
|
code: "DE",
|
|
dailyWorkingHours: 8,
|
|
},
|
|
Hungary: {
|
|
code: "HU",
|
|
dailyWorkingHours: 8,
|
|
},
|
|
India: {
|
|
code: "IN",
|
|
dailyWorkingHours: 9,
|
|
},
|
|
Italy: {
|
|
code: "IT",
|
|
dailyWorkingHours: 8,
|
|
},
|
|
Portugal: {
|
|
code: "PT",
|
|
dailyWorkingHours: 8,
|
|
},
|
|
Spain: {
|
|
code: "ES",
|
|
dailyWorkingHours: 8,
|
|
scheduleRules: {
|
|
type: "spain",
|
|
fridayHours: 6.5,
|
|
summerPeriod: { from: "07-01", to: "09-15" },
|
|
summerHours: 6.5,
|
|
regularHours: 9,
|
|
},
|
|
},
|
|
"United Kingdom": {
|
|
code: "GB",
|
|
dailyWorkingHours: 8,
|
|
},
|
|
} as const satisfies Record<
|
|
string,
|
|
{
|
|
code: string;
|
|
dailyWorkingHours: number;
|
|
scheduleRules?: Prisma.InputJsonValue;
|
|
}
|
|
>;
|
|
|
|
const CLIENT_CODE_OVERRIDES = {
|
|
BMW: "BMW",
|
|
DAIMLER: "DAIMLER",
|
|
"EXOR-STELLANTIS": "STELLANTIS",
|
|
VOLKSWAGEN: "VW",
|
|
"TATA MOTORS GROUP": "JLR",
|
|
} as const satisfies Record<string, string>;
|
|
|
|
const NULLISH_TOKENS = new Set(["", "-", "0", "(Blank)"]);
|
|
|
|
function collapseWhitespace(value: string): string {
|
|
return value.trim().replace(/\s+/g, " ");
|
|
}
|
|
|
|
export function normalizeText(value: unknown): string | null {
|
|
if (value === null || value === undefined) {
|
|
return null;
|
|
}
|
|
|
|
const normalized = collapseWhitespace(String(value));
|
|
return normalized.length > 0 ? normalized : null;
|
|
}
|
|
|
|
export function normalizeNullableWorkbookValue(value: unknown): string | null {
|
|
const normalized = normalizeText(value);
|
|
if (!normalized) {
|
|
return null;
|
|
}
|
|
|
|
return NULLISH_TOKENS.has(normalized) ? null : normalized;
|
|
}
|
|
|
|
export function buildFallbackAccentureEmail(canonicalExternalId: string): string {
|
|
return `${canonicalExternalId}@accenture.com`;
|
|
}
|
|
|
|
export function isPseudoDemandResourceIdentity(value: string | null | undefined): boolean {
|
|
return typeof value === "string" && value.toLowerCase().startsWith("demand_");
|
|
}
|
|
|
|
export function sanitizeClientName(value: string): string {
|
|
return collapseWhitespace(value.replace(/\s*-\s*$/, ""));
|
|
}
|
|
|
|
export function getWorkbookFileName(workbookPath: string): string {
|
|
return path.basename(workbookPath);
|
|
}
|
|
|
|
export function findSectionRow(
|
|
rows: ReadonlyArray<ReadonlyArray<unknown>>,
|
|
firstCellValue: string,
|
|
): number {
|
|
const normalizedTarget = firstCellValue.toLowerCase();
|
|
|
|
for (let index = 0; index < rows.length; index += 1) {
|
|
const current = normalizeText(rows[index]?.[0]);
|
|
if (current?.toLowerCase() === normalizedTarget) {
|
|
return index + 1;
|
|
}
|
|
}
|
|
|
|
throw new Error(`Section row "${firstCellValue}" not found`);
|
|
}
|
|
|
|
export function getCountryReferenceConfig(countryName: string) {
|
|
return COUNTRY_REFERENCE_CONFIG[countryName as keyof typeof COUNTRY_REFERENCE_CONFIG] ?? null;
|
|
}
|
|
|
|
export function deriveCountryCodeFromMetroCity(
|
|
metroCityName: string | null | undefined,
|
|
): string | null {
|
|
if (!metroCityName) {
|
|
return null;
|
|
}
|
|
|
|
for (const [countryName, config] of Object.entries(COUNTRY_REFERENCE_CONFIG)) {
|
|
if (metroCityName === countryName) {
|
|
return config.code;
|
|
}
|
|
|
|
const isGermanCity =
|
|
countryName === "Germany" &&
|
|
["Bonn", "Frankfurt", "Hamburg", "Munich", "Stuttgart"].includes(metroCityName);
|
|
const isPortugueseCity = countryName === "Portugal" && metroCityName === "Lisbon";
|
|
const isUkCity = countryName === "United Kingdom" && metroCityName === "Birmingham";
|
|
const isCostaRica = countryName === "Costa Rica" && metroCityName === "Costa Rica";
|
|
|
|
if (isGermanCity || isPortugueseCity || isUkCity || isCostaRica) {
|
|
return config.code;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function normalizeClientCode(masterClientName: string): string | null {
|
|
return CLIENT_CODE_OVERRIDES[masterClientName as keyof typeof CLIENT_CODE_OVERRIDES] ?? null;
|
|
}
|
|
|
|
export function ensurePercentageValue(value: number | null): number | null {
|
|
if (value === null) {
|
|
return null;
|
|
}
|
|
|
|
return value <= 1 ? Math.round(value * 10000) / 100 : value;
|
|
}
|
|
|
|
export function deriveDisplayNameFromEnterpriseId(enterpriseId: string): string {
|
|
return enterpriseId
|
|
.split(".")
|
|
.filter(Boolean)
|
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(" ");
|
|
}
|
|
|
|
export function deriveRoleTokens(...values: Array<string | null | undefined>): string[] {
|
|
const tokenSet = new Set<string>();
|
|
const combinedValue = values
|
|
.map((value) => normalizeText(value))
|
|
.filter((value): value is string => Boolean(value))
|
|
.join(" ")
|
|
.toUpperCase();
|
|
|
|
if (
|
|
combinedValue.includes("2D") ||
|
|
combinedValue.includes("NUKE") ||
|
|
combinedValue.includes("PHOTOSHOP") ||
|
|
combinedValue.includes("RETOUCH") ||
|
|
combinedValue.includes("ARTWORK") ||
|
|
combinedValue.includes("MOTION DESIGN")
|
|
) {
|
|
tokenSet.add("2D");
|
|
}
|
|
if (combinedValue.includes("3D") || combinedValue.includes("MODELING")) {
|
|
tokenSet.add("3D");
|
|
}
|
|
if (
|
|
combinedValue.includes("PROGRAM/DELIVERY MGMT") ||
|
|
combinedValue.includes("PROJECT MANAGEMENT") ||
|
|
combinedValue.includes("PRODUCER") ||
|
|
combinedValue.includes("HEAD")
|
|
) {
|
|
tokenSet.add("PM");
|
|
}
|
|
if (
|
|
combinedValue.includes("ART DIRECTION") ||
|
|
combinedValue.includes("ART DIRECTOR") ||
|
|
combinedValue.includes("DIGITAL DESIGNER")
|
|
) {
|
|
tokenSet.add("AD");
|
|
}
|
|
|
|
return Array.from(tokenSet);
|
|
}
|
|
|
|
export function deriveNormalizedChapter(
|
|
rawChapter: string | null,
|
|
roleTokens: string[],
|
|
): { chapter: string | null; chapterCode: string | null } {
|
|
const firstRoleToken = roleTokens[0] ?? null;
|
|
if (firstRoleToken) {
|
|
const normalizedChapter = normalizeDispoChapterToken(firstRoleToken);
|
|
if (normalizedChapter) {
|
|
return {
|
|
chapter: normalizedChapter,
|
|
chapterCode: firstRoleToken,
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
chapter: rawChapter,
|
|
chapterCode: null,
|
|
};
|
|
}
|
|
|
|
export function mapChargeabilityResourceType(rawValue: string | null): {
|
|
resourceType: ResourceType;
|
|
warning: string | null;
|
|
} {
|
|
if (!rawValue) {
|
|
return {
|
|
resourceType: ResourceType.EMPLOYEE,
|
|
warning: null,
|
|
};
|
|
}
|
|
|
|
const normalizedValue = rawValue.toLowerCase();
|
|
if (normalizedValue.includes("freelancer")) {
|
|
return { resourceType: ResourceType.FREELANCER, warning: null };
|
|
}
|
|
if (normalizedValue.includes("intern")) {
|
|
return { resourceType: ResourceType.INTERN, warning: null };
|
|
}
|
|
if (normalizedValue.includes("student")) {
|
|
return { resourceType: ResourceType.STUDENT, warning: null };
|
|
}
|
|
if (normalizedValue.includes("apprentice")) {
|
|
return { resourceType: ResourceType.APPRENTICE, warning: null };
|
|
}
|
|
|
|
if (
|
|
normalizedValue === "production studios" ||
|
|
normalizedValue === "near&offshore" ||
|
|
normalizedValue === "accenture" ||
|
|
normalizedValue === "long-term absence"
|
|
) {
|
|
return {
|
|
resourceType: ResourceType.EMPLOYEE,
|
|
warning: null,
|
|
};
|
|
}
|
|
|
|
return {
|
|
resourceType: ResourceType.EMPLOYEE,
|
|
warning: `Unknown MV Ressource Type "${rawValue}" mapped to EMPLOYEE`,
|
|
};
|
|
}
|
|
|
|
export function createAvailabilityFromFte(fte: number | null): Prisma.InputJsonValue {
|
|
return createWeekdayAvailabilityFromFte(fte ?? 1) as unknown as Prisma.InputJsonValue;
|
|
}
|
|
|
|
export function buildBatchSummaryEntry(summary: Record<string, unknown>): Prisma.InputJsonValue {
|
|
return summary as Prisma.InputJsonValue;
|
|
}
|
|
|
|
export async function ensureImportBatch(
|
|
db: Pick<PrismaClient, "importBatch">,
|
|
input: {
|
|
chargeabilitySourceFile?: string;
|
|
importBatchId?: string;
|
|
notes?: string | null;
|
|
planningSourceFile?: string;
|
|
referenceSourceFile?: string;
|
|
},
|
|
): Promise<{ id: string; summary: Record<string, unknown> }> {
|
|
if (input.importBatchId) {
|
|
const existing = await db.importBatch.findUnique({
|
|
where: { id: input.importBatchId },
|
|
select: { id: true, summary: true },
|
|
});
|
|
|
|
if (!existing) {
|
|
throw new Error(`Import batch "${input.importBatchId}" not found`);
|
|
}
|
|
|
|
const updated = await db.importBatch.update({
|
|
where: { id: input.importBatchId },
|
|
data: {
|
|
status: ImportBatchStatus.STAGING,
|
|
...(input.referenceSourceFile !== undefined
|
|
? { referenceSourceFile: input.referenceSourceFile }
|
|
: {}),
|
|
...(input.chargeabilitySourceFile !== undefined
|
|
? { chargeabilitySourceFile: input.chargeabilitySourceFile }
|
|
: {}),
|
|
...(input.planningSourceFile !== undefined
|
|
? { planningSourceFile: input.planningSourceFile }
|
|
: {}),
|
|
...(input.notes !== undefined ? { notes: input.notes } : {}),
|
|
startedAt: new Date(),
|
|
},
|
|
select: { id: true, summary: true },
|
|
});
|
|
|
|
return {
|
|
id: updated.id,
|
|
summary: toJsonObject(updated.summary),
|
|
};
|
|
}
|
|
|
|
const created = await db.importBatch.create({
|
|
data: {
|
|
sourceSystem: "DISPO_V2",
|
|
status: ImportBatchStatus.STAGING,
|
|
...(input.referenceSourceFile !== undefined
|
|
? { referenceSourceFile: input.referenceSourceFile }
|
|
: {}),
|
|
...(input.chargeabilitySourceFile !== undefined
|
|
? { chargeabilitySourceFile: input.chargeabilitySourceFile }
|
|
: {}),
|
|
...(input.planningSourceFile !== undefined
|
|
? { planningSourceFile: input.planningSourceFile }
|
|
: {}),
|
|
...(input.notes !== undefined ? { notes: input.notes } : {}),
|
|
startedAt: new Date(),
|
|
},
|
|
select: { id: true, summary: true },
|
|
});
|
|
|
|
return {
|
|
id: created.id,
|
|
summary: toJsonObject(created.summary),
|
|
};
|
|
}
|
|
|
|
export async function finalizeImportBatchStage(
|
|
db: Pick<PrismaClient, "importBatch">,
|
|
input: {
|
|
batchId: string;
|
|
existingSummary: Record<string, unknown>;
|
|
key: "chargeability" | "planning" | "projectResolution" | "reference" | "roster";
|
|
summary: Record<string, unknown>;
|
|
},
|
|
) {
|
|
const nextSummary = {
|
|
...input.existingSummary,
|
|
[input.key]: input.summary,
|
|
};
|
|
|
|
await db.importBatch.update({
|
|
where: { id: input.batchId },
|
|
data: {
|
|
status: ImportBatchStatus.STAGED,
|
|
stagedAt: new Date(),
|
|
summary: buildBatchSummaryEntry(nextSummary),
|
|
},
|
|
});
|
|
}
|
|
|
|
export function toJsonObject(value: unknown): Record<string, unknown> {
|
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
return value as Record<string, unknown>;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
export function resolveCanonicalEnterpriseIdentity(value: string | null): string | null {
|
|
return value ? normalizeCanonicalResourceIdentity(value) : null;
|
|
}
|
|
|
|
export function createSourceTrace(
|
|
sourceKind: DispoImportSourceKind,
|
|
sourceWorkbook: string,
|
|
sourceSheet: string,
|
|
sourceRow: number,
|
|
sourceColumn?: string | null,
|
|
) {
|
|
return {
|
|
sourceKind,
|
|
sourceWorkbook,
|
|
sourceSheet,
|
|
sourceRow,
|
|
...(sourceColumn !== undefined ? { sourceColumn } : {}),
|
|
};
|
|
}
|
|
|
|
export const DISPO_REFERENCE_SHEET = "EID-Attr";
|
|
export const DISPO_PROJECT_REFERENCE_SHEET = "Project-Attr";
|
|
export const DISPO_CHARGEABILITY_SHEET = "ChgFC";
|
|
export const DISPO_PLANNING_SHEET = "Dispo";
|
|
export const DISPO_ROSTER_SHEET = "DispoRoster";
|
|
export const DISPO_ROSTER_SAP_SHEET = "SAP_data";
|
|
|
|
export { DispoImportSourceKind, DispoStagedRecordType, StagedRecordStatus };
|