@cyc-seattle/platform 1.39.2 → 1.39.4

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.
@@ -22,13 +22,10 @@ export interface DoubleBooked {
22
22
  export declare function enrollmentOf(offering: PlanOffering): number;
23
23
  export declare class Staffing {
24
24
  private readonly ctx;
25
- private readonly covers;
26
25
  private readonly warned;
27
26
  constructor(ctx: Context);
28
27
  /** Warns once for each row and plan copy, about the first date it's seen on. */
29
28
  private warnOnce;
30
- /** Whether holding `held` qualifies for `required`, through `qualification_covers`. */
31
- qualifies(held: Id[], required: Id): boolean;
32
29
  /**
33
30
  * Gathers each block's activities, times and allocations. A block's paid time runs from its first
34
31
  * activity's start to its last one's end, plus the longest setup and put-away. Its time on the
@@ -39,12 +36,11 @@ export declare class Staffing {
39
36
  private checkUnallocated;
40
37
  /** Each program's settings, by its product. */
41
38
  private programs;
42
- /** Whether a staff member holding `held`, at level `levelKey` if any, may coach a program coached at `required`. */
43
- private qualifiedFor;
44
39
  /**
45
40
  * Warns when a block's sailors are more than its staff or its boats can safely take. Each program's
46
- * sailors count against its own sailors per coach, and a coach counts if they're at the staff
47
- * level of one of the block's programs, or qualified for it. Boats count the ones sailors bring.
41
+ * sailors count against its own sailors per coach, and need coaches at its own staff level. A
42
+ * salaried position on the block counts as a coach for any of its programs, since the plan trusts
43
+ * the planner's allocation (#208). Boats count the ones sailors bring.
48
44
  */
49
45
  private checkCapacity;
50
46
  /**
@@ -41,13 +41,9 @@ function groupBy(rows, key) {
41
41
  }
42
42
  export class Staffing {
43
43
  ctx;
44
- covers = new Map();
45
44
  warned = new Set();
46
45
  constructor(ctx) {
47
46
  this.ctx = ctx;
48
- for (const c of ctx.data.qualification_covers) {
49
- this.covers.set(c.qualification_id, new Set([...(this.covers.get(c.qualification_id) ?? []), c.covers_id]));
50
- }
51
47
  }
52
48
  /** Warns once for each row and plan copy, about the first date it's seen on. */
53
49
  warnOnce(code, message, key, scope = {}) {
@@ -57,21 +53,6 @@ export class Staffing {
57
53
  this.warned.add(id);
58
54
  this.ctx.warn(code, message, key, scope);
59
55
  }
60
- /** Whether holding `held` qualifies for `required`, through `qualification_covers`. */
61
- qualifies(held, required) {
62
- const seen = new Set();
63
- const stack = [...held];
64
- while (stack.length > 0) {
65
- const q = stack.pop();
66
- if (q === required)
67
- return true;
68
- if (seen.has(q))
69
- continue;
70
- seen.add(q);
71
- stack.push(...(this.covers.get(q) ?? []));
72
- }
73
- return false;
74
- }
75
56
  /**
76
57
  * Gathers each block's activities, times and allocations. A block's paid time runs from its first
77
58
  * activity's start to its last one's end, plus the longest setup and put-away. Its time on the
@@ -147,18 +128,11 @@ export class Staffing {
147
128
  programs() {
148
129
  return new Map(this.ctx.data.plan_programs.map((p) => [p.product_id, p]));
149
130
  }
150
- /** Whether a staff member holding `held`, at level `levelKey` if any, may coach a program coached at `required`. */
151
- qualifiedFor(held, levelKey, required) {
152
- if (levelKey === required)
153
- return true;
154
- const level = this.ctx.data.plan_staff_levels.find((l) => l.key === required);
155
- const needed = level?.qualifications ?? [];
156
- return needed.length > 0 && needed.every((q) => this.qualifies(held, q));
157
- }
158
131
  /**
159
132
  * Warns when a block's sailors are more than its staff or its boats can safely take. Each program's
160
- * sailors count against its own sailors per coach, and a coach counts if they're at the staff
161
- * level of one of the block's programs, or qualified for it. Boats count the ones sailors bring.
133
+ * sailors count against its own sailors per coach, and need coaches at its own staff level. A
134
+ * salaried position on the block counts as a coach for any of its programs, since the plan trusts
135
+ * the planner's allocation (#208). Boats count the ones sailors bring.
162
136
  */
163
137
  checkCapacity(block, programs) {
164
138
  const { ctx } = this;
@@ -171,9 +145,9 @@ export class Staffing {
171
145
  const scope = { version_id: block.row.version_id, date: block.row.date };
172
146
  // The block's offerings, with the verb that agrees with them: "Guppies Week 2 has".
173
147
  const has = `${offeringsOf(block)} ${new Set(block.members.map((a) => a.offering.id)).size > 1 ? "have" : "has"}`;
174
- let load = 0;
148
+ // Coaches each level needs, from the sailors of the programs coached at it.
149
+ const needed = new Map();
175
150
  let counted = 0;
176
- const required = [];
177
151
  for (const o of offerings) {
178
152
  const settings = programs.get(o.product_id);
179
153
  if (settings?.staff_level_key == null || settings.sailors_per_coach == null || settings.sailors_per_coach <= 0) {
@@ -181,24 +155,25 @@ export class Staffing {
181
155
  continue;
182
156
  }
183
157
  const n = ctx.enrollment.get(o.id) ?? 0;
184
- load += n / settings.sailors_per_coach;
158
+ needed.set(settings.staff_level_key, (needed.get(settings.staff_level_key) ?? 0) + n / settings.sailors_per_coach);
185
159
  counted += n;
186
- required.push(settings.staff_level_key);
187
160
  }
188
161
  if (counted > 0) {
189
- const levels = new Map(ctx.data.plan_staff_levels.map((l) => [l.key, l]));
190
- const positions = new Map(ctx.data.plan_salaried_positions.map((p) => [p.key, p]));
191
- const qualified = sum(block.staff.map((s) => {
192
- const held = s.position_key != null
193
- ? (positions.get(s.position_key)?.qualifications ?? [])
194
- : (levels.get(s.staff_level_key ?? "")?.qualifications ?? []);
195
- const level = s.position_key != null ? null : s.staff_level_key;
196
- return required.some((r) => this.qualifiedFor(held, level, r)) ? s.count : 0;
197
- }));
198
- if (load > qualified + 1e-9) {
199
- ctx.warn("over_capacity", qualified === 0
200
- ? `${has} ${countOf(counted, "sailor")} and no qualified coach`
201
- : `${has} ${countOf(counted, "sailor")}, but enough qualified coaches for only ${Math.floor((qualified * counted) / load + 1e-9)}`, block.row.key, scope);
162
+ const load = sum(needed.values());
163
+ // Each level's staff cover only its own programs; salaried positions cover what's left.
164
+ let short = 0;
165
+ let covered = 0;
166
+ for (const [level, need] of needed) {
167
+ const have = sum(block.staff.filter((s) => s.position_key == null && s.staff_level_key === level).map((s) => s.count));
168
+ covered += Math.min(need, have);
169
+ short += Math.max(0, need - have);
170
+ }
171
+ const salaried = sum(block.staff.filter((s) => s.position_key != null).map((s) => s.count));
172
+ covered += Math.min(short, salaried);
173
+ if (load > covered + 1e-9) {
174
+ ctx.warn("over_capacity", covered === 0
175
+ ? `${has} ${countOf(counted, "sailor")} and no coach`
176
+ : `${has} ${countOf(counted, "sailor")}, but enough coaches for only ${Math.floor((covered * counted) / load + 1e-9)}`, block.row.key, scope);
202
177
  }
203
178
  }
204
179
  if (!block.members.some((a) => a.uses_boats))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyc-seattle/platform",
3
- "version": "1.39.2",
3
+ "version": "1.39.4",
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",