@cyc-seattle/platform 1.22.1 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/planning/bonus.js +2 -0
- package/dist/planning/context.d.ts +2 -0
- package/dist/planning/engine.js +15 -4
- package/dist/planning/lines.js +2 -0
- package/dist/planning/payroll.js +1 -0
- package/dist/planning/pools.js +1 -0
- package/dist/planning/revenue.js +5 -4
- package/dist/planning/salaried.js +2 -0
- package/dist/planning/staffing.d.ts +9 -1
- package/dist/planning/staffing.js +57 -10
- package/dist/planning/types.d.ts +10 -0
- package/dist/schema/index.d.ts +67 -11
- package/package.json +1 -1
package/dist/planning/bonus.js
CHANGED
|
@@ -44,6 +44,7 @@ export function bookBonuses(ctx) {
|
|
|
44
44
|
function: ctx.functionFor(owner.account_id, null, "offering"),
|
|
45
45
|
amount,
|
|
46
46
|
line_key: `bonus:${owner.key}`,
|
|
47
|
+
line_name: `Bonus: ${owner.role_name}`,
|
|
47
48
|
explanation: `${owner.role_name}: ${owner.bonus_rate} × net of its products`,
|
|
48
49
|
stage: "bonus",
|
|
49
50
|
mtdc_share: 0,
|
|
@@ -57,6 +58,7 @@ export function bookBonuses(ctx) {
|
|
|
57
58
|
function: ctx.functionFor(owner.account_id, null, "offering"),
|
|
58
59
|
amount: amount * taxRate,
|
|
59
60
|
line_key: `bonus_payroll_tax:${owner.key}`,
|
|
61
|
+
line_name: `Payroll tax on bonus: ${owner.role_name}`,
|
|
60
62
|
explanation: `${taxRate} × ${owner.role_name}'s bonus`,
|
|
61
63
|
stage: "bonus",
|
|
62
64
|
mtdc_share: 0,
|
|
@@ -9,6 +9,8 @@ export interface Entry {
|
|
|
9
9
|
function: AccountFunction | null;
|
|
10
10
|
amount: number;
|
|
11
11
|
line_key: string;
|
|
12
|
+
/** What the line is, in plain words. */
|
|
13
|
+
line_name: string;
|
|
12
14
|
explanation: string;
|
|
13
15
|
stage: Stage;
|
|
14
16
|
/** How much of the amount MTDC counts: 0 when excluded, a fraction for a mix of rented and owned RIBs. */
|
package/dist/planning/engine.js
CHANGED
|
@@ -7,7 +7,7 @@ import { bookPayrollTax } from "./payroll.js";
|
|
|
7
7
|
import { allocatePools } from "./pools.js";
|
|
8
8
|
import { bookRevenue, countDays } from "./revenue.js";
|
|
9
9
|
import { bookSalaries } from "./salaried.js";
|
|
10
|
-
import { enrollmentOf, Staffing } from "./staffing.js";
|
|
10
|
+
import { enrollmentOf, round2, Staffing } from "./staffing.js";
|
|
11
11
|
/**
|
|
12
12
|
* Calculates the plan year's P&L from a plan's rows. The steps run in the order
|
|
13
13
|
* docs/planning-engine.md gives, each reading only what earlier steps recorded.
|
|
@@ -17,7 +17,7 @@ export function computePlan(data) {
|
|
|
17
17
|
for (const o of data.plan_offerings)
|
|
18
18
|
ctx.enrollment.set(o.id, enrollmentOf(o));
|
|
19
19
|
checkAreas(ctx);
|
|
20
|
-
const { activities, coaching } = new Staffing(ctx).run(scheduleActivities(ctx));
|
|
20
|
+
const { activities, coaching, doubleBooked } = new Staffing(ctx).run(scheduleActivities(ctx));
|
|
21
21
|
countDays(ctx, activities);
|
|
22
22
|
bookRevenue(ctx);
|
|
23
23
|
const booker = new CostCenterBooker(ctx);
|
|
@@ -35,7 +35,7 @@ export function computePlan(data) {
|
|
|
35
35
|
pnl: summarize(ctx, lines, mtdcByOffering),
|
|
36
36
|
organization: organizationTotals(ctx, lines),
|
|
37
37
|
activities,
|
|
38
|
-
positions: summarizePositions(ctx, coaching, charged, bonuses),
|
|
38
|
+
positions: summarizePositions(ctx, coaching, charged, bonuses, doubleBooked),
|
|
39
39
|
warnings: ctx.warnings,
|
|
40
40
|
};
|
|
41
41
|
}
|
|
@@ -66,9 +66,17 @@ function checkAreas(ctx) {
|
|
|
66
66
|
ctx.warn("offering_in_wrong_plan", `Offering ${offering.name} of ${product.name} is in ${where}`, offering.key);
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
const operations = new Set(ctx.data.plan_versions.filter((v) => ctx.planOfVersion.get(v.id)?.kind === "operations").map((v) => v.id));
|
|
70
|
+
for (const version of ctx.data.plan_versions) {
|
|
71
|
+
const pinned = version.operations_version_id;
|
|
72
|
+
if (pinned == null || operations.size === 0 || operations.has(pinned))
|
|
73
|
+
continue;
|
|
74
|
+
const area = ctx.areas.get(ctx.planOfVersion.get(version.id)?.program_area_id ?? "");
|
|
75
|
+
ctx.warn("other_operations_version", `${area?.name ?? "A program area"}'s plan was saved with a different Operations Plan version from the one it's calculated with`, version.id);
|
|
76
|
+
}
|
|
69
77
|
}
|
|
70
78
|
/** Each salaried position's year: its salary, where its hours went, and its hourly wages and bonus. */
|
|
71
|
-
function summarizePositions(ctx, coaching, charged, bonuses) {
|
|
79
|
+
function summarizePositions(ctx, coaching, charged, bonuses, doubleBooked) {
|
|
72
80
|
const taxRate = ctx.rates?.payroll_tax_rate ?? 0;
|
|
73
81
|
const known = new Set(ctx.data.plan_salaried_positions.map((p) => p.key));
|
|
74
82
|
const named = [
|
|
@@ -99,6 +107,8 @@ function summarizePositions(ctx, coaching, charged, bonuses) {
|
|
|
99
107
|
bonus_cents: Math.round(bonus),
|
|
100
108
|
total_pay_cents: Math.round(total),
|
|
101
109
|
payroll_tax_cents: Math.round(total * taxRate),
|
|
110
|
+
double_booked_hours: round2(doubleBooked.get(position.key)?.hours ?? 0),
|
|
111
|
+
double_booked_dates: doubleBooked.get(position.key)?.dates ?? 0,
|
|
102
112
|
};
|
|
103
113
|
});
|
|
104
114
|
}
|
|
@@ -143,6 +153,7 @@ function sumLines(ctx) {
|
|
|
143
153
|
function: e.function,
|
|
144
154
|
amount_cents: 0,
|
|
145
155
|
line_key: e.line_key,
|
|
156
|
+
line_name: e.line_name,
|
|
146
157
|
explanation: e.explanation,
|
|
147
158
|
exact: 0,
|
|
148
159
|
};
|
package/dist/planning/lines.js
CHANGED
|
@@ -74,6 +74,7 @@ export function bookBudgetLines(ctx, booker) {
|
|
|
74
74
|
function: ctx.functionFor(line.account_id, line.function, onOrg ? "org" : line.cost_center_type),
|
|
75
75
|
amount: line.amount_cents,
|
|
76
76
|
line_key: `budget:${line.key}`,
|
|
77
|
+
line_name: line.name,
|
|
77
78
|
explanation: line.name,
|
|
78
79
|
mtdc_share: line.excluded_from_mtdc ? 0 : 1,
|
|
79
80
|
});
|
|
@@ -97,6 +98,7 @@ export function bookLaborLines(ctx, booker) {
|
|
|
97
98
|
function: ctx.functionFor(line.account_id, line.function, line.cost_center_type),
|
|
98
99
|
amount: level.pay_type === "volunteer" ? 0 : line.hours * (level.hourly_rate_cents ?? 0),
|
|
99
100
|
line_key: `labor:${line.key}`,
|
|
101
|
+
line_name: line.name,
|
|
100
102
|
explanation: `${line.name}: hours × ${level.name}'s rate`,
|
|
101
103
|
wages: true,
|
|
102
104
|
});
|
package/dist/planning/payroll.js
CHANGED
package/dist/planning/pools.js
CHANGED
|
@@ -24,6 +24,7 @@ export function allocatePools(ctx) {
|
|
|
24
24
|
account_id: e.account_id,
|
|
25
25
|
function: e.function,
|
|
26
26
|
line_key: `pool:${e.cost_center_key}:${e.line_key}`,
|
|
27
|
+
line_name: `${pool?.name ?? "Pool"} share: ${e.line_name}`,
|
|
27
28
|
explanation: `${pool?.name ?? "Pool"}: ${e.explanation}`,
|
|
28
29
|
stage: "pool",
|
|
29
30
|
mtdc_share: 0,
|
package/dist/planning/revenue.js
CHANGED
|
@@ -31,26 +31,27 @@ export function bookRevenue(ctx) {
|
|
|
31
31
|
ctx.warn("no_revenue_account", `Product ${product.name} has no revenue account`, product.id);
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
|
-
const book = (account, amount, line, explanation, mtdc_share = 1) => ctx.book({
|
|
34
|
+
const book = (account, amount, line, name, explanation, mtdc_share = 1) => ctx.book({
|
|
35
35
|
cost_center_type: "offering",
|
|
36
36
|
cost_center_key: offering.key,
|
|
37
37
|
account_id: account,
|
|
38
38
|
function: ctx.functionFor(account, null, "offering"),
|
|
39
39
|
amount,
|
|
40
40
|
line_key: `${line}:${offering.key}`,
|
|
41
|
+
line_name: name,
|
|
41
42
|
explanation,
|
|
42
43
|
mtdc_share,
|
|
43
44
|
});
|
|
44
|
-
book(revenueAccount, total, "revenue", `${price}¢ × ${enrollment} enrolled`);
|
|
45
|
+
book(revenueAccount, total, "revenue", "Revenue", `${price}¢ × ${enrollment} enrolled`);
|
|
45
46
|
if (offering.cogs_rate != null && offering.cogs_rate > 0) {
|
|
46
47
|
const cogs = ctx.costAccount("cogs");
|
|
47
48
|
if (cogs !== undefined)
|
|
48
|
-
book(cogs, total * offering.cogs_rate, "cogs", `${offering.cogs_rate} × revenue`);
|
|
49
|
+
book(cogs, total * offering.cogs_rate, "cogs", "Cost of goods sold", `${offering.cogs_rate} × revenue`);
|
|
49
50
|
}
|
|
50
51
|
for (const charge of ctx.data.plan_revenue_charges) {
|
|
51
52
|
if (!(charge.product_kinds ?? []).includes(product.kind))
|
|
52
53
|
continue;
|
|
53
|
-
book(charge.account_id, total * charge.rate, `charge:${charge.key}`, `${charge.name}: ${charge.rate} × revenue`, charge.excluded_from_mtdc ? 0 : 1);
|
|
54
|
+
book(charge.account_id, total * charge.rate, `charge:${charge.key}`, charge.name, `${charge.name}: ${charge.rate} × revenue`, charge.excluded_from_mtdc ? 0 : 1);
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -74,6 +74,7 @@ export function bookSalaries(ctx, coaching, booker) {
|
|
|
74
74
|
function: ctx.functionFor(account, null, "offering"),
|
|
75
75
|
amount: c.hours * (rate ?? 0) * scale,
|
|
76
76
|
line_key: `salary:${position.key}:${c.line}`,
|
|
77
|
+
line_name: `${position.name}: ${c.line}`,
|
|
77
78
|
explanation: `${position.name}: ${c.line} hours at the salary's hourly equivalent`,
|
|
78
79
|
wages: true,
|
|
79
80
|
});
|
|
@@ -86,6 +87,7 @@ export function bookSalaries(ctx, coaching, booker) {
|
|
|
86
87
|
function: ctx.functionFor(account, null, "pool"),
|
|
87
88
|
amount: salary - demand * scale,
|
|
88
89
|
line_key: `salary:${position.key}:remainder`,
|
|
90
|
+
line_name: `${position.name}: rest of salary`,
|
|
89
91
|
explanation: `${position.name}: salary not charged to coaching or bookings`,
|
|
90
92
|
wages: true,
|
|
91
93
|
});
|
|
@@ -11,6 +11,13 @@ export interface SalariedCoaching {
|
|
|
11
11
|
/** The wages for `hourly` hours. */
|
|
12
12
|
wages: number;
|
|
13
13
|
}
|
|
14
|
+
/** A salaried position's year of coaching two groups at once. */
|
|
15
|
+
export interface DoubleBooked {
|
|
16
|
+
/** Hours another coach would need: n − 1 for each hour it's on n groups at once. */
|
|
17
|
+
hours: number;
|
|
18
|
+
/** Dates it's on two groups at once. */
|
|
19
|
+
dates: number;
|
|
20
|
+
}
|
|
14
21
|
/** Whole kids: `target_enrollment` if set, otherwise `capacity × fill_rate` rounded. */
|
|
15
22
|
export declare function enrollmentOf(offering: PlanOffering): number;
|
|
16
23
|
export declare class Staffing {
|
|
@@ -42,7 +49,7 @@ export declare class Staffing {
|
|
|
42
49
|
private overlapping;
|
|
43
50
|
/**
|
|
44
51
|
* Warns when a salaried position coaches two activities at once that aren't staffed as one block
|
|
45
|
-
* group. It's still charged to both.
|
|
52
|
+
* group. It's still charged to both. Returns each position's double-booked hours and dates.
|
|
46
53
|
*/
|
|
47
54
|
private checkDoubleBooked;
|
|
48
55
|
/**
|
|
@@ -54,6 +61,7 @@ export declare class Staffing {
|
|
|
54
61
|
run(scheduled: ScheduledActivity[]): {
|
|
55
62
|
activities: ComputedActivity[];
|
|
56
63
|
coaching: SalariedCoaching[];
|
|
64
|
+
doubleBooked: Map<Id, DoubleBooked>;
|
|
57
65
|
};
|
|
58
66
|
/**
|
|
59
67
|
* A trip's coaches each fly and stay; the miles are driven once. Its coaches are those of its
|
|
@@ -231,7 +231,7 @@ export class Staffing {
|
|
|
231
231
|
}
|
|
232
232
|
/**
|
|
233
233
|
* Warns when a salaried position coaches two activities at once that aren't staffed as one block
|
|
234
|
-
* group. It's still charged to both.
|
|
234
|
+
* group. It's still charged to both. Returns each position's double-booked hours and dates.
|
|
235
235
|
*/
|
|
236
236
|
checkDoubleBooked(staffed) {
|
|
237
237
|
const byDay = new Map();
|
|
@@ -244,6 +244,7 @@ export class Staffing {
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
const names = new Map(this.ctx.data.plan_salaried_positions.map((p) => [p.key, p.name]));
|
|
247
|
+
const result = new Map();
|
|
247
248
|
for (const [id, list] of byDay) {
|
|
248
249
|
const [key, date] = id.split("|");
|
|
249
250
|
const clash = list.flatMap((x, i) => list
|
|
@@ -251,8 +252,13 @@ export class Staffing {
|
|
|
251
252
|
.flatMap((y) => ((x.group !== null && x.group === y.group) || !overlaps(x, y) ? [] : [[x, y]])))[0];
|
|
252
253
|
if (clash === undefined)
|
|
253
254
|
continue;
|
|
254
|
-
|
|
255
|
+
const hours = extraCoachMinutes(list) / 60;
|
|
256
|
+
const sum = result.get(key) ?? { hours: 0, dates: 0 };
|
|
257
|
+
result.set(key, { hours: sum.hours + hours, dates: sum.dates + 1 });
|
|
258
|
+
this.ctx.warn("position_double_booked", `${names.get(key) ?? key} coaches ${clash[0].a.offering.name} and ${clash[1].a.offering.name} at the same time on ${date}` +
|
|
259
|
+
(hours > 0 ? `, so another coach is needed for ${round2(hours)} hours` : ""), key);
|
|
255
260
|
}
|
|
261
|
+
return result;
|
|
256
262
|
}
|
|
257
263
|
/**
|
|
258
264
|
* One RIB's hourly and daily cost, averaged over the RIB resources by `quantity`, and the part of
|
|
@@ -299,13 +305,15 @@ export class Staffing {
|
|
|
299
305
|
this.staffBlock(block, group);
|
|
300
306
|
});
|
|
301
307
|
}
|
|
302
|
-
this.checkDoubleBooked(staffed);
|
|
308
|
+
const doubleBooked = this.checkDoubleBooked(staffed);
|
|
303
309
|
const coaching = [];
|
|
304
310
|
const resourcesByActivity = new Map();
|
|
305
311
|
for (const r of ctx.data.plan_activity_resources) {
|
|
306
312
|
resourcesByActivity.set(r.activity_id, [...(resourcesByActivity.get(r.activity_id) ?? []), r]);
|
|
307
313
|
}
|
|
308
314
|
const resources = new Map(ctx.data.plan_resources.map((r) => [r.key, r]));
|
|
315
|
+
const boatClasses = new Map(ctx.data.boat_classes.map((c) => [c.id, c]));
|
|
316
|
+
const salaried = new Map(ctx.data.plan_salaried_positions.map((p) => [p.key, p]));
|
|
309
317
|
const ribRate = this.ribRate();
|
|
310
318
|
for (const s of staffed) {
|
|
311
319
|
const { a } = s;
|
|
@@ -313,7 +321,7 @@ export class Staffing {
|
|
|
313
321
|
const key = offering.key;
|
|
314
322
|
const paidHours = a.paid_minutes / 60;
|
|
315
323
|
const waterHours = a.on_water_minutes / 60;
|
|
316
|
-
const book = (type, amount, line, explanation, mtdcShare = 1) => {
|
|
324
|
+
const book = (type, amount, line, name, explanation, mtdcShare = 1) => {
|
|
317
325
|
if (amount === 0)
|
|
318
326
|
return;
|
|
319
327
|
const account = ctx.costAccount(type);
|
|
@@ -326,6 +334,7 @@ export class Staffing {
|
|
|
326
334
|
function: ctx.functionFor(account, null, "offering"),
|
|
327
335
|
amount,
|
|
328
336
|
line_key: `${line}:${key}`,
|
|
337
|
+
line_name: name,
|
|
329
338
|
explanation,
|
|
330
339
|
mtdc_share: mtdcShare,
|
|
331
340
|
wages: type === "coach_wages",
|
|
@@ -343,8 +352,11 @@ export class Staffing {
|
|
|
343
352
|
}
|
|
344
353
|
const volunteer = s.level?.pay_type === "volunteer";
|
|
345
354
|
const rate = volunteer ? 0 : (s.level?.hourly_rate_cents ?? 0);
|
|
346
|
-
|
|
347
|
-
|
|
355
|
+
// A salaried coach paid by the hour may have their own rate, even on a volunteer level.
|
|
356
|
+
const rateOf = (p) => salaried.get(p.key)?.hourly_coaching_rate_cents ?? rate;
|
|
357
|
+
const own = s.hourlyPositions.reduce((t, p) => t + p.coaches * (rateOf(p) - rate), 0);
|
|
358
|
+
const wages = (hourly * rate + own) * paidHours;
|
|
359
|
+
book("coach_wages", wages, "coach_wages", "Coach wages", "Hourly coaches × paid hours × each one's rate");
|
|
348
360
|
for (const p of s.positions) {
|
|
349
361
|
coaching.push({
|
|
350
362
|
position_key: p.key,
|
|
@@ -356,7 +368,7 @@ export class Staffing {
|
|
|
356
368
|
}
|
|
357
369
|
for (const p of s.hourlyPositions) {
|
|
358
370
|
const hours = p.coaches * paidHours;
|
|
359
|
-
coaching.push({ position_key: p.key, offering_key: key, hours, pay: "hourly", wages: hours *
|
|
371
|
+
coaching.push({ position_key: p.key, offering_key: key, hours, pay: "hourly", wages: hours * rateOf(p) });
|
|
360
372
|
}
|
|
361
373
|
if (s.ribs > 0 && waterHours > 0) {
|
|
362
374
|
if (ribRate === undefined) {
|
|
@@ -365,7 +377,7 @@ export class Staffing {
|
|
|
365
377
|
else {
|
|
366
378
|
const cost = s.ribs * (waterHours * ribRate.hourly + ribRate.daily);
|
|
367
379
|
const rental = s.ribs * (waterHours * ribRate.hourlyRental + ribRate.rental);
|
|
368
|
-
book("ribs", cost, "ribs", "RIBs × on-water hours × the RIB rate, plus a day rate", cost > 0 ? 1 - rental / cost : 1);
|
|
380
|
+
book("ribs", cost, "ribs", "RIBs", "RIBs × on-water hours × the RIB rate, plus a day rate", cost > 0 ? 1 - rental / cost : 1);
|
|
369
381
|
}
|
|
370
382
|
}
|
|
371
383
|
for (const [classId, boats] of s.boats) {
|
|
@@ -374,7 +386,7 @@ export class Staffing {
|
|
|
374
386
|
const boatRate = ctx.data.plan_boat_rates.find((r) => r.boat_class_id === classId);
|
|
375
387
|
if (boatRate === undefined)
|
|
376
388
|
continue;
|
|
377
|
-
book("boat_maintenance", boats * waterHours * boatRate.maintenance_per_boat_hour_cents, `boats:${classId}`, "Boats × on-water hours × the class's maintenance rate");
|
|
389
|
+
book("boat_maintenance", boats * waterHours * boatRate.maintenance_per_boat_hour_cents, `boats:${classId}`, `Boat maintenance: ${boatClasses.get(classId)?.name ?? "boats"}`, "Boats × on-water hours × the class's maintenance rate");
|
|
378
390
|
}
|
|
379
391
|
for (const use of resourcesByActivity.get(a.stored.id) ?? []) {
|
|
380
392
|
const resource = resources.get(use.resource_key);
|
|
@@ -383,12 +395,13 @@ export class Staffing {
|
|
|
383
395
|
continue;
|
|
384
396
|
}
|
|
385
397
|
const hours = use.hours ?? paidHours;
|
|
386
|
-
book("resources", use.quantity * (hours * hourlyCost(resource) + (resource.cost_per_day_cents ?? 0)), `resource:${use.resource_key}`, `${resource.name}: quantity × hours × hourly cost, plus a day rate`, resource.excluded_from_mtdc ? 0 : 1);
|
|
398
|
+
book("resources", use.quantity * (hours * hourlyCost(resource) + (resource.cost_per_day_cents ?? 0)), `resource:${use.resource_key}`, resource.name, `${resource.name}: quantity × hours × hourly cost, plus a day rate`, resource.excluded_from_mtdc ? 0 : 1);
|
|
387
399
|
}
|
|
388
400
|
}
|
|
389
401
|
this.bookTrips(staffed);
|
|
390
402
|
return {
|
|
391
403
|
coaching,
|
|
404
|
+
doubleBooked,
|
|
392
405
|
activities: staffed.map((s) => ({
|
|
393
406
|
offering_id: s.a.offering.id,
|
|
394
407
|
offering_key: s.a.offering.key,
|
|
@@ -441,6 +454,7 @@ export class Staffing {
|
|
|
441
454
|
function: ctx.functionFor(account, null, "offering"),
|
|
442
455
|
amount,
|
|
443
456
|
line_key: `trip:${trip.key}`,
|
|
457
|
+
line_name: trip.name,
|
|
444
458
|
explanation: `${trip.name}: ${round2(coaches)} coaches, ${trip.nights ?? 0} nights, ${trip.miles ?? 0} miles`,
|
|
445
459
|
});
|
|
446
460
|
}
|
|
@@ -456,6 +470,39 @@ function overlaps(x, y) {
|
|
|
456
470
|
return true;
|
|
457
471
|
return xs < ye && ys < xe;
|
|
458
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Minutes another coach would cover among one position's activities on one date: at each moment
|
|
475
|
+
* it's on n groups, n − 1. A block group staffed together is one. Untimed activities add nothing.
|
|
476
|
+
*/
|
|
477
|
+
function extraCoachMinutes(list) {
|
|
478
|
+
const spans = new Map();
|
|
479
|
+
list.forEach((s, i) => {
|
|
480
|
+
if (s.a.start_minutes === null || s.a.end_minutes === null)
|
|
481
|
+
return;
|
|
482
|
+
const id = s.group ?? `own|${i}`;
|
|
483
|
+
const span = spans.get(id);
|
|
484
|
+
spans.set(id, {
|
|
485
|
+
start: Math.min(span?.start ?? Infinity, s.a.start_minutes),
|
|
486
|
+
end: Math.max(span?.end ?? -Infinity, s.a.end_minutes),
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
const edges = [...spans.values()]
|
|
490
|
+
.flatMap(({ start, end }) => [
|
|
491
|
+
{ at: start, step: 1 },
|
|
492
|
+
{ at: end, step: -1 },
|
|
493
|
+
])
|
|
494
|
+
.sort((x, y) => x.at - y.at || x.step - y.step);
|
|
495
|
+
let on = 0;
|
|
496
|
+
let last = 0;
|
|
497
|
+
let minutes = 0;
|
|
498
|
+
for (const { at, step } of edges) {
|
|
499
|
+
if (on > 1)
|
|
500
|
+
minutes += (on - 1) * (at - last);
|
|
501
|
+
on += step;
|
|
502
|
+
last = at;
|
|
503
|
+
}
|
|
504
|
+
return minutes;
|
|
505
|
+
}
|
|
459
506
|
function hourlyCost(r) {
|
|
460
507
|
return (r.cost_per_hour_cents ?? 0) + (r.fuel_per_hour_cents ?? 0) + (r.maintenance_per_hour_cents ?? 0);
|
|
461
508
|
}
|
package/dist/planning/types.d.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface Plan {
|
|
|
83
83
|
export interface PlanVersion {
|
|
84
84
|
id: Id;
|
|
85
85
|
plan_id: Id;
|
|
86
|
+
/** A saved area plan's: the Operations Plan version it was saved with. */
|
|
87
|
+
operations_version_id?: Id | null;
|
|
86
88
|
}
|
|
87
89
|
export type ActivityKind = "practice" | "regatta" | "clinic" | "travel";
|
|
88
90
|
export interface PlanOffering extends Versioned {
|
|
@@ -196,6 +198,8 @@ export interface PlanSalariedPosition extends Versioned {
|
|
|
196
198
|
/** The pool that takes the salary its hours don't charge to programs. */
|
|
197
199
|
remainder_pool_id: Id | null;
|
|
198
200
|
form_990_line: string | null;
|
|
201
|
+
/** What it's paid an hour to coach an offering that pays it by the hour, instead of the offering's level. */
|
|
202
|
+
hourly_coaching_rate_cents?: number | null;
|
|
199
203
|
}
|
|
200
204
|
/** Hours charged to a product, or to a program area and shared among its offerings: one of the two. */
|
|
201
205
|
export interface PlanSalariedBooking extends Versioned {
|
|
@@ -313,6 +317,8 @@ export interface CostLine {
|
|
|
313
317
|
/** Always positive: the account's type says whether it's revenue or expense. */
|
|
314
318
|
amount_cents: number;
|
|
315
319
|
line_key: string;
|
|
320
|
+
/** What the line is, in plain words, such as "G&A share: Payroll tax". */
|
|
321
|
+
line_name: string;
|
|
316
322
|
explanation: string;
|
|
317
323
|
}
|
|
318
324
|
/** Revenue, expenses and net, in cents. */
|
|
@@ -393,6 +399,10 @@ export interface PositionSummary {
|
|
|
393
399
|
total_pay_cents: number;
|
|
394
400
|
/** The year's payroll tax on its total pay. */
|
|
395
401
|
payroll_tax_cents: number;
|
|
402
|
+
/** Hours another coach would need while it's on two or more groups at once: n − 1 for n groups. */
|
|
403
|
+
double_booked_hours: number;
|
|
404
|
+
/** Dates it's on two or more groups at once. */
|
|
405
|
+
double_booked_dates: number;
|
|
396
406
|
}
|
|
397
407
|
export interface EngineOutput {
|
|
398
408
|
lines: CostLine[];
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface Schema {
|
|
|
67
67
|
resource_kinds: ResourceKinds[];
|
|
68
68
|
sync_runs: SyncRuns[];
|
|
69
69
|
sync_tasks: SyncTasks[];
|
|
70
|
+
version_set_members: VersionSetMembers[];
|
|
71
|
+
version_sets: VersionSets[];
|
|
70
72
|
}
|
|
71
73
|
/** The chart of accounts that plans, results, and actuals book to. */
|
|
72
74
|
export interface Accounts {
|
|
@@ -685,8 +687,8 @@ export interface PlanResources {
|
|
|
685
687
|
/** Each cost and revenue line in a saved version's P&L. Worked out when the version was saved, and never changed. */
|
|
686
688
|
export interface PlanResultLines {
|
|
687
689
|
id: string;
|
|
688
|
-
/** The saved version these results are for. */
|
|
689
|
-
version_id: string | PlanVersions;
|
|
690
|
+
/** The saved version these results are for, if they aren't a club plan's. */
|
|
691
|
+
version_id: string | PlanVersions | null;
|
|
690
692
|
cost_center_type: "offering" | "product" | "program_area" | "pool" | "org";
|
|
691
693
|
cost_center_key: string | null;
|
|
692
694
|
cost_center_name: string;
|
|
@@ -694,15 +696,21 @@ export interface PlanResultLines {
|
|
|
694
696
|
function: "program" | "management" | "fundraising" | null;
|
|
695
697
|
/** Always positive: the account says whether it's revenue or a cost. In cents: 12500 is $125.00. */
|
|
696
698
|
amount_cents: number;
|
|
697
|
-
/**
|
|
699
|
+
/** The engine's key for the line. */
|
|
698
700
|
line: string;
|
|
699
701
|
explanation: string | null;
|
|
702
|
+
/** The club plan these results are for, if they aren't a version's. */
|
|
703
|
+
version_set_id: string | VersionSets | null;
|
|
704
|
+
/** In a club plan's results, the plan that holds what it's charged to. */
|
|
705
|
+
version_set_member_id: string | VersionSetMembers | null;
|
|
706
|
+
/** What the line is, in plain words, such as G&A share: Payroll tax. */
|
|
707
|
+
line_name: string | null;
|
|
700
708
|
}
|
|
701
709
|
/** A saved version's P&L for each offering, product, and program area. Worked out when the version was saved, and never changed. */
|
|
702
710
|
export interface PlanResultPnl {
|
|
703
711
|
id: string;
|
|
704
|
-
/** The saved version these results are for. */
|
|
705
|
-
version_id: string | PlanVersions;
|
|
712
|
+
/** The saved version these results are for, if they aren't a club plan's. */
|
|
713
|
+
version_id: string | PlanVersions | null;
|
|
706
714
|
level: "offering" | "product" | "program_area";
|
|
707
715
|
subject_key: string;
|
|
708
716
|
name: string;
|
|
@@ -717,12 +725,16 @@ export interface PlanResultPnl {
|
|
|
717
725
|
enrollment: number;
|
|
718
726
|
days: number;
|
|
719
727
|
sailor_days: number;
|
|
728
|
+
/** The club plan these results are for, if they aren't a version's. */
|
|
729
|
+
version_set_id: string | VersionSets | null;
|
|
730
|
+
/** In a club plan's results, the plan that holds what it's charged to. */
|
|
731
|
+
version_set_member_id: string | VersionSetMembers | null;
|
|
720
732
|
}
|
|
721
733
|
/** Each salaried position's pay for the year, in a saved version. Worked out when the version was saved, and never changed. */
|
|
722
734
|
export interface PlanResultPositions {
|
|
723
735
|
id: string;
|
|
724
|
-
/** The saved version these results are for. */
|
|
725
|
-
version_id: string | PlanVersions;
|
|
736
|
+
/** The saved version these results are for, if they aren't a club plan's. */
|
|
737
|
+
version_id: string | PlanVersions | null;
|
|
726
738
|
position_key: string;
|
|
727
739
|
name: string;
|
|
728
740
|
/** In cents: 12500 is $125.00. */
|
|
@@ -742,6 +754,12 @@ export interface PlanResultPositions {
|
|
|
742
754
|
total_pay_cents: number;
|
|
743
755
|
/** The year's payroll tax on the total pay. In cents: 12500 is $125.00. */
|
|
744
756
|
payroll_tax_cents: number;
|
|
757
|
+
/** The club plan these results are for, if they aren't a version's. */
|
|
758
|
+
version_set_id: string | VersionSets | null;
|
|
759
|
+
/** Hours another coach would cover while this position coaches two or more groups at once: for three groups at once, two coaches. */
|
|
760
|
+
double_booked_hours: string | null;
|
|
761
|
+
/** Dates this position coaches two or more groups at once. */
|
|
762
|
+
double_booked_dates: number | null;
|
|
745
763
|
}
|
|
746
764
|
/** The other saved versions a version's results were worked out with. Worked out when the version was saved, and never changed. */
|
|
747
765
|
export interface PlanResultVersions {
|
|
@@ -754,18 +772,20 @@ export interface PlanResultVersions {
|
|
|
754
772
|
/** What the P&L warned about when a version was saved. Worked out when the version was saved, and never changed. */
|
|
755
773
|
export interface PlanResultWarnings {
|
|
756
774
|
id: string;
|
|
757
|
-
/** The saved version these results are for. */
|
|
758
|
-
version_id: string | PlanVersions;
|
|
775
|
+
/** The saved version these results are for, if they aren't a club plan's. */
|
|
776
|
+
version_id: string | PlanVersions | null;
|
|
759
777
|
/** The kind of warning. */
|
|
760
778
|
code: string;
|
|
761
779
|
message: string;
|
|
762
780
|
subject_key: string | null;
|
|
781
|
+
/** The club plan these results are for, if they aren't a version's. */
|
|
782
|
+
version_set_id: string | VersionSets | null;
|
|
763
783
|
}
|
|
764
784
|
/** A saved version's club-wide totals. Worked out when the version was saved, and never changed. */
|
|
765
785
|
export interface PlanResults {
|
|
766
786
|
id: string;
|
|
767
|
-
/** The saved version these results are for. */
|
|
768
|
-
version_id: string | PlanVersions;
|
|
787
|
+
/** The saved version these results are for, if they aren't a club plan's. */
|
|
788
|
+
version_id: string | PlanVersions | null;
|
|
769
789
|
calculated_at: string;
|
|
770
790
|
/** In cents: 12500 is $125.00. */
|
|
771
791
|
operating_revenue_cents: number;
|
|
@@ -783,6 +803,8 @@ export interface PlanResults {
|
|
|
783
803
|
capital_cents: number;
|
|
784
804
|
/** Operating net, plus fundraising net, less capital. In cents: 12500 is $125.00. */
|
|
785
805
|
total_cents: number;
|
|
806
|
+
/** The club plan these results are for, if they aren't a version's. */
|
|
807
|
+
version_set_id: string | VersionSets | null;
|
|
786
808
|
}
|
|
787
809
|
/** A charge set as a share of each offering's revenue, such as scholarships or card fees. Operations Plan only. */
|
|
788
810
|
export interface PlanRevenueCharges {
|
|
@@ -835,6 +857,8 @@ export interface PlanSalariedPositions {
|
|
|
835
857
|
/** The pool that takes the salary its hours don't charge to programs. */
|
|
836
858
|
remainder_pool_id: string | PlanPools | null;
|
|
837
859
|
form_990_line: string | null;
|
|
860
|
+
/** What this position is paid an hour to coach an offering that pays its salaried coach by the hour, instead of the offering's coach level. Leave empty to use the level. In cents: 12500 is $125.00. */
|
|
861
|
+
hourly_coaching_rate_cents: number | null;
|
|
838
862
|
}
|
|
839
863
|
/** A level hourly staff are paid at, such as head coach. */
|
|
840
864
|
export interface PlanStaffLevels {
|
|
@@ -960,6 +984,8 @@ export interface Programs {
|
|
|
960
984
|
name: string;
|
|
961
985
|
/** The product this program belongs to. Its kind must be Program. */
|
|
962
986
|
product_id: string | Products;
|
|
987
|
+
/** A team, such as a race team. Families on a team see everyone on every team. */
|
|
988
|
+
team: boolean;
|
|
963
989
|
/** Who has a role in this program. */
|
|
964
990
|
role_assignments: (string | ProgramRoleAssignments)[];
|
|
965
991
|
/** The program's Google Group, if it has one. Most programs don't. */
|
|
@@ -1024,3 +1050,33 @@ export interface SyncTasks {
|
|
|
1024
1050
|
/** Set once the task has failed too many times. It keeps retrying; this flags it for staff. */
|
|
1025
1051
|
needs_attention: boolean;
|
|
1026
1052
|
}
|
|
1053
|
+
/** One plan's version in a club plan. Set when the club plan is saved, and never changed. */
|
|
1054
|
+
export interface VersionSetMembers {
|
|
1055
|
+
id: string;
|
|
1056
|
+
version_set_id: string | VersionSets;
|
|
1057
|
+
plan_id: string | Plans;
|
|
1058
|
+
/** The plan's active version when the club plan was saved. */
|
|
1059
|
+
version_id: string | PlanVersions;
|
|
1060
|
+
}
|
|
1061
|
+
/** The club's plan for a year, saved: the active Operations Plan version and each program area's, with the P&L they make together. Saved when it's added, and never changed. */
|
|
1062
|
+
export interface VersionSets {
|
|
1063
|
+
id: string;
|
|
1064
|
+
/** The organization the club plan is for. */
|
|
1065
|
+
organization_id: string | Organizations;
|
|
1066
|
+
/** The year the club plan is for. */
|
|
1067
|
+
fiscal_year: number;
|
|
1068
|
+
/** Such as 2027 budget as adopted. */
|
|
1069
|
+
name: string;
|
|
1070
|
+
/** Why it was saved. */
|
|
1071
|
+
message: string | null;
|
|
1072
|
+
/** Set when the club plan is saved. */
|
|
1073
|
+
saved_by: string | null;
|
|
1074
|
+
/** Set when the club plan is saved. */
|
|
1075
|
+
saved_at: string | null;
|
|
1076
|
+
/** The version of the planning engine that calculated its numbers. */
|
|
1077
|
+
platform_version: string | null;
|
|
1078
|
+
/** The active version of each plan when the club plan was saved. */
|
|
1079
|
+
members: (string | VersionSetMembers)[];
|
|
1080
|
+
/** What the club's P&L showed when it was saved, worked out a moment after. A club plan with none means working them out failed, and an admin can fill them in. */
|
|
1081
|
+
results: (string | PlanResults)[];
|
|
1082
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyc-seattle/platform",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"description": "Pulumi components for CYC's shared Google Cloud platform: WebService, the platform outputs reader, and the stacks that build the platform",
|
|
5
5
|
"author": "CYC Community Sailing Center",
|
|
6
6
|
"license": "Apache-2.0",
|