chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import type {
|
||||
EstimateDemandLine,
|
||||
EstimateDemandLineCalculationMetadata,
|
||||
EstimateDemandLineRateMode,
|
||||
EstimateDemandSummary,
|
||||
} from "@planarchy/shared";
|
||||
|
||||
export interface EstimateDemandLineRateSnapshot {
|
||||
resourceId?: string | null;
|
||||
currency?: string | null;
|
||||
lcrCents: number;
|
||||
ucrCents: number;
|
||||
}
|
||||
|
||||
export interface EstimateDemandLineForCalculation {
|
||||
resourceId?: string | null | undefined;
|
||||
hours: number;
|
||||
rateSource?: string | null | undefined;
|
||||
costRateCents: number;
|
||||
billRateCents: number;
|
||||
currency?: string | null | undefined;
|
||||
costTotalCents: number;
|
||||
priceTotalCents: number;
|
||||
metadata?: Record<string, unknown> | null | undefined;
|
||||
}
|
||||
|
||||
type ParsedDemandLineMetadata = Record<string, unknown> & {
|
||||
calculation?: Partial<EstimateDemandLineCalculationMetadata>;
|
||||
};
|
||||
|
||||
function parseRateMode(value: unknown): EstimateDemandLineRateMode | undefined {
|
||||
return value === "resource" || value === "manual" ? value : undefined;
|
||||
}
|
||||
|
||||
function parseDemandLineMetadata(
|
||||
metadata: Record<string, unknown> | null | undefined,
|
||||
): ParsedDemandLineMetadata {
|
||||
const safeMetadata =
|
||||
typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)
|
||||
? metadata
|
||||
: {};
|
||||
const rawCalculation =
|
||||
typeof safeMetadata.calculation === "object" &&
|
||||
safeMetadata.calculation !== null &&
|
||||
!Array.isArray(safeMetadata.calculation)
|
||||
? (safeMetadata.calculation as Record<string, unknown>)
|
||||
: undefined;
|
||||
|
||||
if (!rawCalculation) {
|
||||
return safeMetadata as ParsedDemandLineMetadata;
|
||||
}
|
||||
|
||||
const calculation: Partial<EstimateDemandLineCalculationMetadata> = {};
|
||||
|
||||
const costRateMode = parseRateMode(rawCalculation.costRateMode);
|
||||
if (costRateMode) {
|
||||
calculation.costRateMode = costRateMode;
|
||||
}
|
||||
|
||||
const billRateMode = parseRateMode(rawCalculation.billRateMode);
|
||||
if (billRateMode) {
|
||||
calculation.billRateMode = billRateMode;
|
||||
}
|
||||
|
||||
if (rawCalculation.totalMode === "computed") {
|
||||
calculation.totalMode = "computed";
|
||||
}
|
||||
|
||||
if (typeof rawCalculation.liveCostRateCents === "number") {
|
||||
calculation.liveCostRateCents = rawCalculation.liveCostRateCents;
|
||||
}
|
||||
|
||||
if (typeof rawCalculation.liveBillRateCents === "number") {
|
||||
calculation.liveBillRateCents = rawCalculation.liveBillRateCents;
|
||||
}
|
||||
|
||||
if (typeof rawCalculation.liveCurrency === "string") {
|
||||
calculation.liveCurrency = rawCalculation.liveCurrency;
|
||||
}
|
||||
|
||||
return {
|
||||
...safeMetadata,
|
||||
...(Object.keys(calculation).length > 0 ? { calculation } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function inferRateMode(
|
||||
resourceSnapshot: EstimateDemandLineRateSnapshot | null | undefined,
|
||||
effectiveRateCents: number,
|
||||
liveRateCents: number | undefined,
|
||||
explicitMode: EstimateDemandLineRateMode | undefined,
|
||||
): EstimateDemandLineRateMode {
|
||||
if (explicitMode) {
|
||||
return explicitMode;
|
||||
}
|
||||
|
||||
if (!resourceSnapshot || liveRateCents == null) {
|
||||
return "manual";
|
||||
}
|
||||
|
||||
return effectiveRateCents === liveRateCents ? "resource" : "manual";
|
||||
}
|
||||
|
||||
export function getEstimateDemandLineCalculationMetadata(
|
||||
line: Pick<
|
||||
EstimateDemandLineForCalculation,
|
||||
"resourceId" | "costRateCents" | "billRateCents" | "metadata"
|
||||
>,
|
||||
options?: {
|
||||
resourceSnapshot?: EstimateDemandLineRateSnapshot | null | undefined;
|
||||
},
|
||||
): EstimateDemandLineCalculationMetadata {
|
||||
const parsedMetadata = parseDemandLineMetadata(line.metadata);
|
||||
const explicitCalculation = parsedMetadata.calculation;
|
||||
const resourceSnapshot = options?.resourceSnapshot;
|
||||
const liveCostRateCents = resourceSnapshot?.lcrCents;
|
||||
const liveBillRateCents = resourceSnapshot?.ucrCents;
|
||||
|
||||
return {
|
||||
costRateMode: inferRateMode(
|
||||
resourceSnapshot,
|
||||
line.costRateCents,
|
||||
liveCostRateCents,
|
||||
explicitCalculation?.costRateMode,
|
||||
),
|
||||
billRateMode: inferRateMode(
|
||||
resourceSnapshot,
|
||||
line.billRateCents,
|
||||
liveBillRateCents,
|
||||
explicitCalculation?.billRateMode,
|
||||
),
|
||||
totalMode: "computed",
|
||||
liveCostRateCents: liveCostRateCents ?? null,
|
||||
liveBillRateCents: liveBillRateCents ?? null,
|
||||
liveCurrency: resourceSnapshot?.currency ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeEstimateDemandLine<T extends EstimateDemandLineForCalculation>(
|
||||
line: T,
|
||||
options?: {
|
||||
resourceSnapshot?: EstimateDemandLineRateSnapshot | null | undefined;
|
||||
defaultCurrency?: string;
|
||||
},
|
||||
): T {
|
||||
const resourceSnapshot = options?.resourceSnapshot;
|
||||
const calculation = getEstimateDemandLineCalculationMetadata(line, {
|
||||
resourceSnapshot,
|
||||
});
|
||||
const effectiveCostRateCents =
|
||||
calculation.costRateMode === "resource" && resourceSnapshot
|
||||
? resourceSnapshot.lcrCents
|
||||
: line.costRateCents;
|
||||
const effectiveBillRateCents =
|
||||
calculation.billRateMode === "resource" && resourceSnapshot
|
||||
? resourceSnapshot.ucrCents
|
||||
: line.billRateCents;
|
||||
const currency =
|
||||
((calculation.costRateMode === "resource" ||
|
||||
calculation.billRateMode === "resource") &&
|
||||
resourceSnapshot?.currency
|
||||
? resourceSnapshot.currency
|
||||
: line.currency) ||
|
||||
resourceSnapshot?.currency ||
|
||||
options?.defaultCurrency ||
|
||||
"EUR";
|
||||
const metadata = parseDemandLineMetadata(line.metadata);
|
||||
|
||||
return {
|
||||
...line,
|
||||
costRateCents: effectiveCostRateCents,
|
||||
billRateCents: effectiveBillRateCents,
|
||||
currency,
|
||||
costTotalCents: Math.round(line.hours * effectiveCostRateCents),
|
||||
priceTotalCents: Math.round(line.hours * effectiveBillRateCents),
|
||||
metadata: {
|
||||
...metadata,
|
||||
calculation,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function summarizeEstimateDemandLines(
|
||||
demandLines: Pick<
|
||||
EstimateDemandLine,
|
||||
"hours" | "costTotalCents" | "priceTotalCents"
|
||||
>[],
|
||||
): EstimateDemandSummary {
|
||||
const totalHours = demandLines.reduce((sum, line) => sum + line.hours, 0);
|
||||
const totalCostCents = demandLines.reduce(
|
||||
(sum, line) => sum + line.costTotalCents,
|
||||
0,
|
||||
);
|
||||
const totalPriceCents = demandLines.reduce(
|
||||
(sum, line) => sum + line.priceTotalCents,
|
||||
0,
|
||||
);
|
||||
const marginCents = totalPriceCents - totalCostCents;
|
||||
const marginPercent =
|
||||
totalPriceCents > 0 ? Math.round((marginCents / totalPriceCents) * 100) : 0;
|
||||
|
||||
return {
|
||||
totalHours,
|
||||
totalCostCents,
|
||||
totalPriceCents,
|
||||
marginCents,
|
||||
marginPercent,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user