@cyc-seattle/platform 1.39.1 → 1.39.3
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/activities.js +3 -3
- package/dist/planning/bonus.js +1 -1
- package/dist/planning/context.js +14 -3
- package/dist/planning/engine.js +7 -5
- package/dist/planning/lines.js +8 -5
- package/dist/planning/pools.js +1 -1
- package/dist/planning/revenue.js +2 -2
- package/dist/planning/salaried.js +3 -3
- package/dist/planning/staffing.js +31 -20
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@ export function scheduleActivities(ctx) {
|
|
|
9
9
|
for (const c of ctx.data.plan_closed_dates ?? []) {
|
|
10
10
|
// The closed days are the Operations Plan's, not an area's.
|
|
11
11
|
if (ctx.planOfVersion.get(c.version_id)?.kind === "program_area") {
|
|
12
|
-
ctx.warn("closed_date_in_area_plan", `${c.name} is a closed day in a
|
|
12
|
+
ctx.warn("closed_date_in_area_plan", `${c.name} is a closed day in a Program Plan. Only the Operations Plan's closed days count`, c.key);
|
|
13
13
|
continue;
|
|
14
14
|
}
|
|
15
15
|
closed.set(c.date, c.name);
|
|
@@ -18,7 +18,7 @@ export function scheduleActivities(ctx) {
|
|
|
18
18
|
for (const activity of ctx.data.plan_activities) {
|
|
19
19
|
const offering = ctx.offeringsById.get(activity.offering_id);
|
|
20
20
|
if (offering === undefined) {
|
|
21
|
-
ctx.warn("unknown_offering", `Activity ${activity.key}
|
|
21
|
+
ctx.warn("unknown_offering", `Activity ${activity.key} is for an offering that isn't in the plan`, activity.key);
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
24
|
if (!ctx.inYear(offering))
|
|
@@ -32,7 +32,7 @@ export function scheduleActivities(ctx) {
|
|
|
32
32
|
const span = start !== null && end !== null ? end - start : 0;
|
|
33
33
|
const closedFor = closed.get(activity.date);
|
|
34
34
|
if (closedFor !== undefined && activity.kind !== "travel") {
|
|
35
|
-
ctx.warn("closed_date", `${offering.name}'s ${activity.kind}
|
|
35
|
+
ctx.warn("closed_date", `${offering.name}'s ${activity.kind} is on ${closedFor}, when ${ctx.data.organization.name} is closed`, activity.key, {
|
|
36
36
|
version_id: activity.version_id,
|
|
37
37
|
date: activity.date,
|
|
38
38
|
});
|
package/dist/planning/bonus.js
CHANGED
|
@@ -79,7 +79,7 @@ function baseOf(ctx, owner) {
|
|
|
79
79
|
continue;
|
|
80
80
|
const product = ctx.products.get(row.product_id);
|
|
81
81
|
if (plan?.kind === "program_area" && product?.program_area_id !== plan.program_area_id) {
|
|
82
|
-
ctx.warn("bonus_outside_area", `${owner.role_name}'s bonus names ${product?.name ?? row.product_id}, which isn't in
|
|
82
|
+
ctx.warn("bonus_outside_area", `${owner.role_name}'s bonus names ${product?.name ?? row.product_id}, which isn't in this plan's program area, so it's left out`, owner.key);
|
|
83
83
|
continue;
|
|
84
84
|
}
|
|
85
85
|
products.add(row.product_id);
|
package/dist/planning/context.js
CHANGED
|
@@ -8,6 +8,17 @@ const ALLOCATION_WARNINGS = new Set([
|
|
|
8
8
|
"closed_date",
|
|
9
9
|
"position_double_booked",
|
|
10
10
|
]);
|
|
11
|
+
/** Each cost type as a warning names it. */
|
|
12
|
+
const COST_TYPES = {
|
|
13
|
+
coach_wages: "coach wages",
|
|
14
|
+
salaries: "salaries",
|
|
15
|
+
payroll_tax: "payroll tax",
|
|
16
|
+
ribs: "RIBs",
|
|
17
|
+
boat_maintenance: "boat maintenance",
|
|
18
|
+
resources: "resources",
|
|
19
|
+
travel: "travel",
|
|
20
|
+
cogs: "cost of goods sold",
|
|
21
|
+
};
|
|
11
22
|
/** Lookups over the input, and the entries recorded so far, shared by every step. */
|
|
12
23
|
export class Context {
|
|
13
24
|
/** The plan year's first and last days. */
|
|
@@ -43,7 +54,7 @@ export class Context {
|
|
|
43
54
|
this.offeringsByKey = new Map(data.plan_offerings.map((o) => [o.key, o]));
|
|
44
55
|
this.rates = data.plan_rates[0];
|
|
45
56
|
if (data.plan_rates.length > 1)
|
|
46
|
-
this.warn("many_rates", "
|
|
57
|
+
this.warn("many_rates", "The Operations Plan has more than one set of rates. Only the first one counts");
|
|
47
58
|
this.costAccounts = new Map(data.organization_cost_accounts
|
|
48
59
|
.filter((c) => c.organization_id === data.organization.id)
|
|
49
60
|
.map((c) => [c.cost_type, c.account_id]));
|
|
@@ -52,7 +63,7 @@ export class Context {
|
|
|
52
63
|
costAccount(type) {
|
|
53
64
|
const account = this.costAccounts.get(type);
|
|
54
65
|
if (account === undefined) {
|
|
55
|
-
this.warn("no_cost_account",
|
|
66
|
+
this.warn("no_cost_account", `${this.data.organization.name} has no account for ${COST_TYPES[type]}, so that cost is left out`, type);
|
|
56
67
|
}
|
|
57
68
|
return account;
|
|
58
69
|
}
|
|
@@ -109,7 +120,7 @@ export class Context {
|
|
|
109
120
|
if (area === undefined)
|
|
110
121
|
return product.revenue_account_id;
|
|
111
122
|
if (product.revenue_account_id != null && product.revenue_account_id !== area.revenue_account_id) {
|
|
112
|
-
this.warn("revenue_account_differs",
|
|
123
|
+
this.warn("revenue_account_differs", `${product.name} has its own revenue account, but its revenue goes to ${area.name}'s account`, product.id);
|
|
113
124
|
}
|
|
114
125
|
return area.revenue_account_id;
|
|
115
126
|
}
|
package/dist/planning/engine.js
CHANGED
|
@@ -50,10 +50,10 @@ function checkAreas(ctx) {
|
|
|
50
50
|
if (product === undefined)
|
|
51
51
|
continue;
|
|
52
52
|
if (product.kind === "program" && product.program_area_id == null) {
|
|
53
|
-
ctx.warn("program_without_area",
|
|
53
|
+
ctx.warn("program_without_area", `${product.name} is a program but has no program area`, product.id);
|
|
54
54
|
}
|
|
55
55
|
else if (product.kind !== "program" && product.program_area_id != null) {
|
|
56
|
-
ctx.warn("area_on_non_program",
|
|
56
|
+
ctx.warn("area_on_non_program", `${product.name} isn't a program but has a program area`, product.id);
|
|
57
57
|
}
|
|
58
58
|
const plan = ctx.planOfVersion.get(offering.version_id);
|
|
59
59
|
if (plan === undefined)
|
|
@@ -62,8 +62,10 @@ function checkAreas(ctx) {
|
|
|
62
62
|
? product.program_area_id != null
|
|
63
63
|
: product.program_area_id != null && product.program_area_id !== plan.program_area_id;
|
|
64
64
|
if (wrong) {
|
|
65
|
-
const
|
|
66
|
-
|
|
65
|
+
const message = plan.kind === "operations"
|
|
66
|
+
? `${offering.name} is in the Operations Plan, but its program is in a program area`
|
|
67
|
+
: `${offering.name} is in the Program Plan for another program area`;
|
|
68
|
+
ctx.warn("offering_in_wrong_plan", message, offering.key);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -77,7 +79,7 @@ function summarizePositions(ctx, coaching, charged, bonuses, doubleBooked) {
|
|
|
77
79
|
];
|
|
78
80
|
for (const key of new Set(named)) {
|
|
79
81
|
if (!known.has(key)) {
|
|
80
|
-
ctx.warn("unknown_position", `A block or bonus names
|
|
82
|
+
ctx.warn("unknown_position", `A block or bonus names salaried position ${key}, which isn't in the plan`, key);
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
return ctx.data.plan_salaried_positions.map((position) => {
|
package/dist/planning/lines.js
CHANGED
|
@@ -31,8 +31,11 @@ export class CostCenterBooker {
|
|
|
31
31
|
pending.forEach((entry, i) => {
|
|
32
32
|
const w = weights[i] ?? null;
|
|
33
33
|
if (w === null) {
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const key = entry.cost_center_key ?? "";
|
|
35
|
+
const name = entry.cost_center_type === "product"
|
|
36
|
+
? (this.ctx.products.get(key)?.name ?? key)
|
|
37
|
+
: (this.ctx.areas.get(key)?.name ?? key);
|
|
38
|
+
this.ctx.warn(`${entry.cost_center_type}_without_offerings`, `${name} has no offerings this year, so ${entry.line_name} isn't split among offerings`, entry.cost_center_key ?? undefined);
|
|
36
39
|
this.ctx.book(entry);
|
|
37
40
|
return;
|
|
38
41
|
}
|
|
@@ -49,7 +52,7 @@ function checkCostCenter(ctx, type, key, line) {
|
|
|
49
52
|
(type === "program_area" && key != null && ctx.areas.has(key)) ||
|
|
50
53
|
(type === "pool" && key != null && ctx.data.plan_pools.some((p) => p.key === key));
|
|
51
54
|
if (!known)
|
|
52
|
-
ctx.warn("unknown_cost_center", `Line ${line} is
|
|
55
|
+
ctx.warn("unknown_cost_center", `Line ${line} is charged to ${type.replace("_", " ")} ${key ?? ""}, which isn't in the plan`, line);
|
|
53
56
|
return known;
|
|
54
57
|
}
|
|
55
58
|
/**
|
|
@@ -65,7 +68,7 @@ export function bookBudgetLines(ctx, booker) {
|
|
|
65
68
|
const capital = ctx.accountType(line.account_id) === "asset";
|
|
66
69
|
const onOrg = line.cost_center_type === "org" || capital;
|
|
67
70
|
if (capital && line.cost_center_type !== "org") {
|
|
68
|
-
ctx.warn("capital_off_org",
|
|
71
|
+
ctx.warn("capital_off_org", `${line.name} is capital, so it's charged to ${ctx.data.organization.name}, which pays for all capital`, line.key);
|
|
69
72
|
}
|
|
70
73
|
booker.add({
|
|
71
74
|
cost_center_type: onOrg ? "org" : line.cost_center_type,
|
|
@@ -88,7 +91,7 @@ export function bookLaborLines(ctx, booker) {
|
|
|
88
91
|
continue;
|
|
89
92
|
const level = levels.get(line.staff_level_key);
|
|
90
93
|
if (level === undefined) {
|
|
91
|
-
ctx.warn("unknown_staff_level", `Labor line ${line.key}
|
|
94
|
+
ctx.warn("unknown_staff_level", `Labor line ${line.key} has a staff level that isn't in the Operations Plan, so it's left out`, line.key);
|
|
92
95
|
continue;
|
|
93
96
|
}
|
|
94
97
|
booker.add({
|
package/dist/planning/pools.js
CHANGED
|
@@ -16,7 +16,7 @@ export function allocatePools(ctx) {
|
|
|
16
16
|
return;
|
|
17
17
|
ctx.entries.splice(0, ctx.entries.length, ...kept);
|
|
18
18
|
if (total <= 0) {
|
|
19
|
-
ctx.warn("no_mtdc",
|
|
19
|
+
ctx.warn("no_mtdc", `No offering this year has costs that count toward MTDC, so ${ctx.data.organization.name} keeps all the shared costs`);
|
|
20
20
|
}
|
|
21
21
|
for (const e of pooled) {
|
|
22
22
|
const pool = pools.get(e.cost_center_key ?? "");
|
package/dist/planning/revenue.js
CHANGED
|
@@ -18,7 +18,7 @@ export function bookRevenue(ctx) {
|
|
|
18
18
|
for (const offering of ctx.fyOfferings()) {
|
|
19
19
|
const product = ctx.products.get(offering.product_id);
|
|
20
20
|
if (product === undefined) {
|
|
21
|
-
ctx.warn("unknown_product", `Offering ${offering.key}
|
|
21
|
+
ctx.warn("unknown_product", `Offering ${offering.key} is for a product that doesn't exist`, offering.key);
|
|
22
22
|
continue;
|
|
23
23
|
}
|
|
24
24
|
const enrollment = ctx.enrollment.get(offering.id) ?? 0;
|
|
@@ -28,7 +28,7 @@ export function bookRevenue(ctx) {
|
|
|
28
28
|
continue;
|
|
29
29
|
const revenueAccount = ctx.revenueAccount(product);
|
|
30
30
|
if (revenueAccount == null) {
|
|
31
|
-
ctx.warn("no_revenue_account",
|
|
31
|
+
ctx.warn("no_revenue_account", `${product.name} has no revenue account, so its revenue is left out`, product.id);
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
34
|
const book = (account, amount, line, name, explanation, mtdc_share = 1) => ctx.book({
|
|
@@ -29,7 +29,7 @@ export function bookSalaries(ctx, coaching, booker) {
|
|
|
29
29
|
.filter((b) => b.position_id === position.id)
|
|
30
30
|
.flatMap((b) => {
|
|
31
31
|
if ((b.product_id == null) === (b.program_area_id == null)) {
|
|
32
|
-
ctx.warn("booking_cost_center", `Booking ${b.key}
|
|
32
|
+
ctx.warn("booking_cost_center", `Booking ${b.key} needs a product or a program area, but not both. It's left out`, b.key);
|
|
33
33
|
return [];
|
|
34
34
|
}
|
|
35
35
|
return [
|
|
@@ -45,13 +45,13 @@ export function bookSalaries(ctx, coaching, booker) {
|
|
|
45
45
|
}),
|
|
46
46
|
];
|
|
47
47
|
if (rate === null && charges.length > 0) {
|
|
48
|
-
ctx.warn("no_paid_hours", `${position.name} has hours charged to programs but no
|
|
48
|
+
ctx.warn("no_paid_hours", `${position.name} has hours charged to programs but no paid hours per year, so those hours cost nothing`, position.key);
|
|
49
49
|
}
|
|
50
50
|
const available = salary * (1 - (position.ga_share ?? 0));
|
|
51
51
|
const demand = charges.reduce((t, c) => t + c.hours * (rate ?? 0), 0);
|
|
52
52
|
const scale = demand > available && demand > 0 ? available / demand : 1;
|
|
53
53
|
if (scale < 1) {
|
|
54
|
-
ctx.warn("position_overbooked", `${position.name}'s hours charged to programs cost more than its salary
|
|
54
|
+
ctx.warn("position_overbooked", `${position.name}'s hours charged to programs cost more than its salary less its G&A share, so they're cut to fit`, position.key);
|
|
55
55
|
}
|
|
56
56
|
const sums = { coaching_hours: 0, coaching_cents: 0, booking_hours: 0, booking_cents: 0 };
|
|
57
57
|
for (const c of charges) {
|
|
@@ -15,8 +15,15 @@ function sum(values) {
|
|
|
15
15
|
* ends in "s", such as RIBs; any other name is used as given, since adding an "s" to "Laser Radial" reads wrong.
|
|
16
16
|
*/
|
|
17
17
|
function countOf(n, name) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
return `${n} ${n === 1 ? name : pluralOf(name)}`;
|
|
19
|
+
}
|
|
20
|
+
function pluralOf(name) {
|
|
21
|
+
return /^\S+$/.test(name) && !/s$/i.test(name) ? `${name}s` : name;
|
|
22
|
+
}
|
|
23
|
+
const day = new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", timeZone: "UTC" });
|
|
24
|
+
/** A date as a person says it: "July 13". */
|
|
25
|
+
function dayOf(date) {
|
|
26
|
+
return day.format(new Date(`${date}T00:00:00Z`));
|
|
20
27
|
}
|
|
21
28
|
/** Names as a sentence lists them: `a`, `a and b`, or `a, b, and c`. */
|
|
22
29
|
function listOf(names) {
|
|
@@ -83,11 +90,11 @@ export class Staffing {
|
|
|
83
90
|
continue;
|
|
84
91
|
const row = rows.get(id);
|
|
85
92
|
if (row === undefined) {
|
|
86
|
-
this.ctx.warn("unknown_block",
|
|
93
|
+
this.ctx.warn("unknown_block", `${a.offering.name}'s ${a.kind} is in a block that isn't in the plan`, a.stored.key);
|
|
87
94
|
continue;
|
|
88
95
|
}
|
|
89
96
|
if (row.date !== a.date) {
|
|
90
|
-
this.ctx.warn("block_date_mismatch", `${a.offering.name}'s ${a.kind} is in ${row.name}, which is on ${row.date}`, a.stored.key, { version_id: a.stored.version_id, date: a.date });
|
|
97
|
+
this.ctx.warn("block_date_mismatch", `${a.offering.name}'s ${a.kind} is in ${row.name}, which is on ${dayOf(row.date)}`, a.stored.key, { version_id: a.stored.version_id, date: a.date });
|
|
91
98
|
}
|
|
92
99
|
members.set(id, [...(members.get(id) ?? []), a]);
|
|
93
100
|
}
|
|
@@ -162,14 +169,15 @@ export class Staffing {
|
|
|
162
169
|
if (sailors <= 0)
|
|
163
170
|
return;
|
|
164
171
|
const scope = { version_id: block.row.version_id, date: block.row.date };
|
|
165
|
-
|
|
172
|
+
// The block's offerings, with the verb that agrees with them: "Guppies Week 2 has".
|
|
173
|
+
const has = `${offeringsOf(block)} ${new Set(block.members.map((a) => a.offering.id)).size > 1 ? "have" : "has"}`;
|
|
166
174
|
let load = 0;
|
|
167
175
|
let counted = 0;
|
|
168
176
|
const required = [];
|
|
169
177
|
for (const o of offerings) {
|
|
170
178
|
const settings = programs.get(o.product_id);
|
|
171
179
|
if (settings?.staff_level_key == null || settings.sailors_per_coach == null || settings.sailors_per_coach <= 0) {
|
|
172
|
-
this.warnOnce("no_program_settings", `${o.name}'s program has no staff level or sailors per coach
|
|
180
|
+
this.warnOnce("no_program_settings", `${o.name}'s program has no staff level or sailors per coach. Add both to check that it has enough coaches`, o.product_id, { version_id: o.version_id, date: block.row.date });
|
|
173
181
|
continue;
|
|
174
182
|
}
|
|
175
183
|
const n = ctx.enrollment.get(o.id) ?? 0;
|
|
@@ -188,7 +196,9 @@ export class Staffing {
|
|
|
188
196
|
return required.some((r) => this.qualifiedFor(held, level, r)) ? s.count : 0;
|
|
189
197
|
}));
|
|
190
198
|
if (load > qualified + 1e-9) {
|
|
191
|
-
ctx.warn("over_capacity",
|
|
199
|
+
ctx.warn("over_capacity", qualified === 0
|
|
200
|
+
? `${has} ${countOf(counted, "sailor")} and no coach`
|
|
201
|
+
: `${has} ${countOf(counted, "sailor")}, but enough coaches for only ${Math.floor((qualified * counted) / load + 1e-9)}`, block.row.key, scope);
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
204
|
if (!block.members.some((a) => a.uses_boats))
|
|
@@ -198,13 +208,15 @@ export class Staffing {
|
|
|
198
208
|
for (const b of block.boats) {
|
|
199
209
|
const perBoat = classes.get(b.boat_class_id)?.sailors_per_boat;
|
|
200
210
|
if (perBoat == null || perBoat <= 0) {
|
|
201
|
-
this.warnOnce("no_sailors_per_boat",
|
|
211
|
+
this.warnOnce("no_sailors_per_boat", `${classes.get(b.boat_class_id)?.name ?? b.boat_class_id} has no sailors per boat. Add it to check that the boats hold every sailor`, b.boat_class_id, scope);
|
|
202
212
|
continue;
|
|
203
213
|
}
|
|
204
214
|
seats += (b.count + (b.sailor_owned ?? 0)) * perBoat;
|
|
205
215
|
}
|
|
206
216
|
if (sailors > seats) {
|
|
207
|
-
ctx.warn("over_capacity",
|
|
217
|
+
ctx.warn("over_capacity", seats === 0
|
|
218
|
+
? `${has} ${countOf(sailors, "sailor")} and no boats`
|
|
219
|
+
: `${has} ${countOf(sailors, "sailor")}, but the boats hold only ${seats}`, block.row.key, scope);
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
/**
|
|
@@ -226,10 +238,9 @@ export class Staffing {
|
|
|
226
238
|
const peak = mostAtOnce(out);
|
|
227
239
|
if (peak.total <= have)
|
|
228
240
|
return;
|
|
229
|
-
const
|
|
230
|
-
?
|
|
231
|
-
:
|
|
232
|
-
const message = `${countOf(peak.total, what)} out, but ${organization} has ${have}${whose}`;
|
|
241
|
+
const message = peak.at.length === 1
|
|
242
|
+
? `${countOf(peak.total, what)} out at once for ${offeringsOf(peak.at[0].block)}, but ${organization} has only ${have}`
|
|
243
|
+
: `${countOf(peak.total, what)} out at once, but ${organization} has only ${have}: ${listOf(peak.at.map(({ block, n }) => `${n} for ${offeringsOf(block)}`))}`;
|
|
233
244
|
for (const version of new Set(peak.at.map(({ block }) => block.row.version_id))) {
|
|
234
245
|
ctx.warn(code, message, what, {
|
|
235
246
|
version_id: version,
|
|
@@ -241,7 +252,7 @@ export class Staffing {
|
|
|
241
252
|
for (const id of classIds) {
|
|
242
253
|
if (fleet.get(id) == null) {
|
|
243
254
|
for (const b of list.filter((x) => x.boats.some((y) => y.boat_class_id === id && y.count > 0))) {
|
|
244
|
-
this.warnOnce("no_fleet_quantity", `The Operations Plan doesn't say how many ${classes.get(id)?.name ?? id}
|
|
255
|
+
this.warnOnce("no_fleet_quantity", `The Operations Plan doesn't say how many ${pluralOf(classes.get(id)?.name ?? id)} ${organization} has. Add it to check that there are enough`, id, { version_id: b.row.version_id, date });
|
|
245
256
|
}
|
|
246
257
|
}
|
|
247
258
|
check("over_inventory_boats", classes.get(id)?.name ?? id, fleet.get(id), (b) => sum(b.boats.filter((x) => x.boat_class_id === id).map((x) => x.count)));
|
|
@@ -339,7 +350,7 @@ export class Staffing {
|
|
|
339
350
|
if (s.pay === "hourly") {
|
|
340
351
|
const rate = position?.hourly_coaching_rate_cents;
|
|
341
352
|
if (position !== undefined && rate == null) {
|
|
342
|
-
this.warnOnce("no_hourly_rate", `${position.name} is paid by the hour to coach but has no hourly rate, so those hours cost nothing`, position.key, scope);
|
|
353
|
+
this.warnOnce("no_hourly_rate", `${position.name} is paid by the hour to coach but has no own hourly coaching rate, so those hours cost nothing`, position.key, scope);
|
|
343
354
|
}
|
|
344
355
|
wages += hours * (rate ?? 0);
|
|
345
356
|
for (const [a, share] of block.shares) {
|
|
@@ -367,7 +378,7 @@ export class Staffing {
|
|
|
367
378
|
}
|
|
368
379
|
const level = s.staff_level_key == null ? undefined : levels.get(s.staff_level_key);
|
|
369
380
|
if (level === undefined) {
|
|
370
|
-
this.warnOnce("unknown_staff_level", `${block.row.name} has staff at a
|
|
381
|
+
this.warnOnce("unknown_staff_level", `${block.row.name} has staff at a staff level that isn't in the Operations Plan, so they cost nothing`, s.staff_level_key ?? s.key, scope);
|
|
371
382
|
continue;
|
|
372
383
|
}
|
|
373
384
|
if (level.pay_type !== "volunteer")
|
|
@@ -378,7 +389,7 @@ export class Staffing {
|
|
|
378
389
|
for (const use of block.resources) {
|
|
379
390
|
const resource = resources.get(use.resource_key);
|
|
380
391
|
if (resource === undefined) {
|
|
381
|
-
this.warnOnce("unknown_resource", `${block.row.name} uses
|
|
392
|
+
this.warnOnce("unknown_resource", `${block.row.name} uses resource ${use.resource_key}, which isn't in the plan`, use.resource_key, scope);
|
|
382
393
|
continue;
|
|
383
394
|
}
|
|
384
395
|
const rib = ribKind != null && resource.resource_kind_id === ribKind;
|
|
@@ -439,15 +450,15 @@ export class Staffing {
|
|
|
439
450
|
.filter(travel)
|
|
440
451
|
.map((a) => a.date)
|
|
441
452
|
.sort()[0];
|
|
442
|
-
this.warnOnce("trip_without_staff", `${trip.name} has no staff on
|
|
453
|
+
this.warnOnce("trip_without_staff", `${trip.name} has no staff on its travel days, so the plan has no airfare or lodging for its coaches`, trip.key, { version_id: trip.version_id, ...(first !== undefined && { date: first }) });
|
|
443
454
|
}
|
|
444
455
|
const lodging = ctx.rates?.lodging_per_night_cents ?? null;
|
|
445
456
|
const mileage = ctx.rates?.mileage_rate_cents ?? null;
|
|
446
457
|
if ((trip.nights ?? 0) > 0 && lodging === null) {
|
|
447
|
-
this.warnOnce("no_rate", "Trips have nights but no
|
|
458
|
+
this.warnOnce("no_rate", "Trips have nights, but the Operations Plan has no lodging per night", "lodging_per_night_cents");
|
|
448
459
|
}
|
|
449
460
|
if ((trip.miles ?? 0) > 0 && mileage === null) {
|
|
450
|
-
this.warnOnce("no_rate", "Trips have miles but no
|
|
461
|
+
this.warnOnce("no_rate", "Trips have miles, but the Operations Plan has no mileage rate", "mileage_rate_cents");
|
|
451
462
|
}
|
|
452
463
|
const amount = coaches * (trip.airfare_cents ?? 0) +
|
|
453
464
|
coaches * (trip.nights ?? 0) * (lodging ?? 0) +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyc-seattle/platform",
|
|
3
|
-
"version": "1.39.
|
|
3
|
+
"version": "1.39.3",
|
|
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",
|