@updraft-solutions/mcrit-sdk 0.1.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.
Files changed (79) hide show
  1. package/README.md +240 -0
  2. package/dist/chunk-2DRKSYW4.cjs +99 -0
  3. package/dist/chunk-2M32EOYI.js +145 -0
  4. package/dist/chunk-AN55SAGU.js +1189 -0
  5. package/dist/chunk-D5DUVW34.js +176 -0
  6. package/dist/chunk-HQSF3WGF.cjs +14 -0
  7. package/dist/chunk-JGIAOBZI.js +99 -0
  8. package/dist/chunk-JRVRIKNM.cjs +168 -0
  9. package/dist/chunk-LVTDNDWE.cjs +124 -0
  10. package/dist/chunk-LXNCAKJZ.js +0 -0
  11. package/dist/chunk-MF7G76QS.cjs +176 -0
  12. package/dist/chunk-MI4YBENQ.cjs +145 -0
  13. package/dist/chunk-MOOGM3DW.cjs +1 -0
  14. package/dist/chunk-NQV2D3FJ.js +14 -0
  15. package/dist/chunk-TJXWL42D.js +124 -0
  16. package/dist/chunk-VYT5KQWF.js +168 -0
  17. package/dist/chunk-XKZQ5W7E.cjs +1189 -0
  18. package/dist/courses/index.cjs +7 -0
  19. package/dist/courses/index.d.cts +58 -0
  20. package/dist/courses/index.d.ts +58 -0
  21. package/dist/courses/index.js +7 -0
  22. package/dist/crew/index.cjs +10 -0
  23. package/dist/crew/index.d.cts +102 -0
  24. package/dist/crew/index.d.ts +102 -0
  25. package/dist/crew/index.js +10 -0
  26. package/dist/crew/vue.cjs +34 -0
  27. package/dist/crew/vue.d.cts +31 -0
  28. package/dist/crew/vue.d.ts +31 -0
  29. package/dist/crew/vue.js +34 -0
  30. package/dist/format/index.cjs +44 -0
  31. package/dist/format/index.d.cts +69 -0
  32. package/dist/format/index.d.ts +69 -0
  33. package/dist/format/index.js +44 -0
  34. package/dist/index.cjs +261 -0
  35. package/dist/index.d.cts +11 -0
  36. package/dist/index.d.ts +11 -0
  37. package/dist/index.js +261 -0
  38. package/dist/media/index.cjs +30 -0
  39. package/dist/media/index.d.cts +89 -0
  40. package/dist/media/index.d.ts +89 -0
  41. package/dist/media/index.js +30 -0
  42. package/dist/models-CY3MmvCg.d.cts +33801 -0
  43. package/dist/models-CY3MmvCg.d.ts +33801 -0
  44. package/dist/money/index.cjs +18 -0
  45. package/dist/money/index.d.cts +66 -0
  46. package/dist/money/index.d.ts +66 -0
  47. package/dist/money/index.js +18 -0
  48. package/dist/pagination/index.cjs +6 -0
  49. package/dist/pagination/index.d.cts +16 -0
  50. package/dist/pagination/index.d.ts +16 -0
  51. package/dist/pagination/index.js +6 -0
  52. package/dist/schemas/index.cjs +158 -0
  53. package/dist/schemas/index.d.cts +1019 -0
  54. package/dist/schemas/index.d.ts +1019 -0
  55. package/dist/schemas/index.js +158 -0
  56. package/dist/types/index.cjs +1 -0
  57. package/dist/types/index.d.cts +143 -0
  58. package/dist/types/index.d.ts +143 -0
  59. package/dist/types/index.js +1 -0
  60. package/package.json +171 -0
  61. package/src/courses/index.ts +202 -0
  62. package/src/crew/index.ts +255 -0
  63. package/src/crew/vue.ts +70 -0
  64. package/src/format/index.ts +265 -0
  65. package/src/index.ts +76 -0
  66. package/src/media/index.ts +224 -0
  67. package/src/money/index.ts +168 -0
  68. package/src/pagination/index.ts +27 -0
  69. package/src/schemas/common.ts +73 -0
  70. package/src/schemas/faq.ts +94 -0
  71. package/src/schemas/fee.ts +37 -0
  72. package/src/schemas/index.ts +6 -0
  73. package/src/schemas/models.ts +996 -0
  74. package/src/schemas/report.ts +306 -0
  75. package/src/schemas/safety-report.ts +163 -0
  76. package/src/types/api.ts +37 -0
  77. package/src/types/compliance.ts +44 -0
  78. package/src/types/index.ts +3 -0
  79. package/src/types/models.ts +124 -0
@@ -0,0 +1,202 @@
1
+ import type { Dinero as DineroType } from "dinero.js";
2
+ import { groupBy } from "lodash-es";
3
+
4
+ import type { FeeLike, NetGross, ParsedPrice } from "../money/index.js";
5
+ import { money, parseFee } from "../money/index.js";
6
+
7
+ // Re-export commonly used money types so SDK consumers can import everything
8
+ // they need from a single `@updraft-solutions/mcrit-sdk/courses` path.
9
+ export type { NetGross, FeeLike, PriceLike, ParsedFee, ParsedPrice } from "../money/index.js";
10
+
11
+ /**
12
+ * Permissive input shape for {@link parseCourse}. Course fees in
13
+ * different parts of the API are sometimes returned with pivot data
14
+ * nested in `pivot`, sometimes flattened on the fee object itself —
15
+ * this type supports both, and an optional nested `fee` field for the
16
+ * eager-loaded fee resource.
17
+ */
18
+ export interface CourseFeeLike extends FeeLike {
19
+ pivot?: {
20
+ count?: number;
21
+ variant?: string | null;
22
+ is_optional?: boolean;
23
+ sort_order?: number;
24
+ };
25
+ count?: number;
26
+ variant?: string | null;
27
+ is_optional?: boolean;
28
+ sort_order?: number;
29
+ fee?: FeeLike;
30
+ }
31
+
32
+ /**
33
+ * Permissive input shape for {@link parseCourse}.
34
+ */
35
+ export interface CourseLike {
36
+ id?: number;
37
+ fees?: CourseFeeLike[];
38
+ [key: string]: unknown;
39
+ }
40
+
41
+ export interface ParsedCourseFee {
42
+ fee: CourseFeeLike;
43
+ count: number;
44
+ variant: string;
45
+ isOptional: boolean;
46
+ sortOrder: number;
47
+ unitPrice: DineroType | null;
48
+ lineTotal: DineroType | null;
49
+ title: string;
50
+ hasUpcomingPrice: boolean;
51
+ upcomingPrice: DineroType | null;
52
+ upcomingPriceValidFrom: string | null;
53
+ }
54
+
55
+ export interface CourseVariant {
56
+ variant: string;
57
+ fees: ParsedCourseFee[];
58
+ totalCost: DineroType;
59
+ }
60
+
61
+ export interface ParsedCourse {
62
+ totalCost(netGross?: NetGross): DineroType | null;
63
+ feesWithCost(netGross?: NetGross): ParsedCourseFee[];
64
+ variants(netGross?: NetGross): CourseVariant[];
65
+ }
66
+
67
+ const DEFAULT_VARIANT = "default";
68
+
69
+ function getPivotData(courseFee: CourseFeeLike) {
70
+ return {
71
+ count: courseFee.pivot?.count ?? courseFee.count ?? 1,
72
+ variant: courseFee.pivot?.variant ?? courseFee.variant ?? null,
73
+ isOptional: courseFee.pivot?.is_optional ?? courseFee.is_optional ?? false,
74
+ sortOrder: courseFee.pivot?.sort_order ?? courseFee.sort_order ?? 0,
75
+ };
76
+ }
77
+
78
+ function getFeeData(courseFee: CourseFeeLike): FeeLike {
79
+ return courseFee.fee ?? courseFee;
80
+ }
81
+
82
+ function getCourseFeeTitle(courseFee: CourseFeeLike): string {
83
+ const feeData = getFeeData(courseFee);
84
+ if (feeData.title) {
85
+ return feeData.title;
86
+ }
87
+ if (feeData.ident) {
88
+ return feeData.ident;
89
+ }
90
+ if (courseFee.id) {
91
+ return `Fee #${courseFee.id}`;
92
+ }
93
+ return "Unknown fee";
94
+ }
95
+
96
+ function sortBySortOrder<T extends { sortOrder: number }>(items: T[]): T[] {
97
+ return [...items].sort((a, b) => a.sortOrder - b.sortOrder);
98
+ }
99
+
100
+ function calculateTotal(parsedFees: ParsedCourseFee[]): DineroType {
101
+ return parsedFees.reduce<DineroType>(
102
+ (total, pf) => (pf.lineTotal ? total.add(pf.lineTotal) : total),
103
+ money(0),
104
+ );
105
+ }
106
+
107
+ function asDinero(price: DineroType | ParsedPrice | null): DineroType | null {
108
+ if (!price) {
109
+ return null;
110
+ }
111
+ if ("multiply" in price && typeof price.multiply === "function") {
112
+ return price as DineroType;
113
+ }
114
+ return null;
115
+ }
116
+
117
+ function parseCourseFee(courseFee: CourseFeeLike, netGross: NetGross): ParsedCourseFee {
118
+ const feeData = getFeeData(courseFee);
119
+ const pivotData = getPivotData(courseFee);
120
+ const parsed = parseFee(feeData);
121
+
122
+ const unitPrice = asDinero(parsed?.currentPrice(netGross) ?? null);
123
+ const lineTotal = unitPrice ? unitPrice.multiply(pivotData.count) : null;
124
+
125
+ const upcomingRaw = parsed?.upcomingPrice(null) ?? null;
126
+ const hasUpcomingPrice = upcomingRaw !== null;
127
+ const upcomingParsed = upcomingRaw && !("multiply" in upcomingRaw)
128
+ ? (upcomingRaw as ParsedPrice)
129
+ : null;
130
+ const upcomingPrice = upcomingParsed
131
+ ? (netGross === "net" ? upcomingParsed.net_total : upcomingParsed.gross_total)
132
+ : null;
133
+
134
+ return {
135
+ fee: courseFee,
136
+ count: pivotData.count,
137
+ variant: pivotData.variant || DEFAULT_VARIANT,
138
+ isOptional: pivotData.isOptional,
139
+ sortOrder: pivotData.sortOrder,
140
+ unitPrice,
141
+ lineTotal,
142
+ title: getCourseFeeTitle(courseFee),
143
+ hasUpcomingPrice,
144
+ upcomingPrice,
145
+ upcomingPriceValidFrom: upcomingParsed?.valid_from ?? null,
146
+ };
147
+ }
148
+
149
+ export function parseCourse(course: CourseLike | null | undefined): ParsedCourse | null {
150
+ if (!course) {
151
+ return null;
152
+ }
153
+
154
+ return {
155
+ totalCost(netGross: NetGross = "gross") {
156
+ const fees = course.fees ?? [];
157
+ if (fees.length === 0) {
158
+ return null;
159
+ }
160
+ const parsed = fees.map((f) => parseCourseFee(f, netGross));
161
+ const total = calculateTotal(parsed);
162
+ return total.getAmount() === 0 ? null : total;
163
+ },
164
+
165
+ feesWithCost(netGross: NetGross = "gross") {
166
+ const fees = course.fees ?? [];
167
+ return sortBySortOrder(fees.map((f) => parseCourseFee(f, netGross)));
168
+ },
169
+
170
+ variants(netGross: NetGross = "gross") {
171
+ const fees = course.fees ?? [];
172
+ if (fees.length === 0) {
173
+ return [];
174
+ }
175
+
176
+ const parsed = fees.map((f) => parseCourseFee(f, netGross));
177
+ const grouped = groupBy(parsed, (pf) => pf.variant);
178
+ const commonFees = grouped[DEFAULT_VARIANT] ?? [];
179
+
180
+ if (Object.keys(grouped).length === 1 && commonFees.length > 0) {
181
+ return [
182
+ {
183
+ variant: DEFAULT_VARIANT,
184
+ fees: sortBySortOrder(commonFees),
185
+ totalCost: calculateTotal(commonFees),
186
+ },
187
+ ];
188
+ }
189
+
190
+ return Object.entries(grouped)
191
+ .filter(([variant]) => variant !== DEFAULT_VARIANT)
192
+ .map(([variant, variantFees]) => {
193
+ const all = sortBySortOrder([...variantFees, ...commonFees]);
194
+ return {
195
+ variant,
196
+ fees: all,
197
+ totalCost: calculateTotal(all),
198
+ };
199
+ });
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Crew configuration consumer for the MCRIT SDK.
3
+ *
4
+ * No crew-role data lives in this module — the canonical source is the
5
+ * backend `App\Crew\CrewConfiguration`, served via `GET /meta/crew`.
6
+ * Apps fetch the config once at boot, hand it to `createCrewValidator`,
7
+ * and use the returned validator to drive UI feedback on a live crew
8
+ * list.
9
+ */
10
+
11
+ export interface PositionAttributes {
12
+ picPriority: number | null;
13
+ isOnBoard: boolean;
14
+ isInstructorClass: boolean;
15
+ isTrainingRole: boolean;
16
+ }
17
+
18
+ export interface PositionDefinition extends PositionAttributes {
19
+ name: string;
20
+ }
21
+
22
+ export type Constraint =
23
+ | { kind: "single_pic" }
24
+ | { kind: "min_members"; min: number }
25
+ | {
26
+ kind: "when_crew_contains_role_requires_one_of";
27
+ when_role: string;
28
+ required_roles: string[];
29
+ message_key: string | null;
30
+ }
31
+ | {
32
+ kind: "when_any_membership_has_system_role_requires_one_of";
33
+ system_role: string;
34
+ required_roles: string[];
35
+ message_key: string | null;
36
+ };
37
+
38
+ export type ConstraintKind = Constraint["kind"];
39
+
40
+ export interface CrewConfig {
41
+ version: number;
42
+ positions: Record<string, PositionDefinition>;
43
+ pic_priority: string[];
44
+ constraints: Constraint[];
45
+ }
46
+
47
+ export interface MembershipRoleHolder {
48
+ roles?: { name: string; company_id?: number | null }[] | null;
49
+ }
50
+
51
+ /**
52
+ * True iff the membership holds the given *system* (global, company-less)
53
+ * role. Pure helper that operates on the membership payload's `roles`
54
+ * array — unrelated to the per-flight crew validator.
55
+ */
56
+ export function hasMembershipRole(
57
+ membership: MembershipRoleHolder | null | undefined,
58
+ roleName: string,
59
+ ): boolean {
60
+ return membership?.roles?.some((r) => r.name === roleName) ?? false;
61
+ }
62
+
63
+ /**
64
+ * UX nicety: smart-default the crew-position dropdown when adding a new
65
+ * crew member, based on the membership's system roles. Returns the crew
66
+ * position string ("student" / "instructor" / "pilot"). Not a validation
67
+ * rule and intentionally divorced from the validator — adding a new
68
+ * default mapping only requires changing this one function.
69
+ */
70
+ export function defaultCrewRoleForMembership(
71
+ membership: MembershipRoleHolder | null | undefined,
72
+ ): string {
73
+ if (hasMembershipRole(membership, "student")) {
74
+ return "student";
75
+ }
76
+ if (hasMembershipRole(membership, "instructor")) {
77
+ return "instructor";
78
+ }
79
+ return "pilot";
80
+ }
81
+
82
+ export interface CrewEntry {
83
+ role: string;
84
+ membership_id?: number | null;
85
+ membership?: MembershipRoleHolder | null;
86
+ }
87
+
88
+ export interface Violation {
89
+ kind: ConstraintKind;
90
+ message_key: string | null;
91
+ details?: Record<string, unknown>;
92
+ }
93
+
94
+ export type PicResolution =
95
+ | { type: "ok"; index: number; role: string }
96
+ | { type: "tie"; role: string }
97
+ | { type: "none" };
98
+
99
+ export interface CrewValidator {
100
+ /** True iff the named role exists in the config and is `isOnBoard`. */
101
+ isOnBoardRole(role: string): boolean;
102
+ isInstructorClassRole(role: string): boolean;
103
+ isPicEligibleRole(role: string): boolean;
104
+ /** Resolves the single PIC by walking `config.pic_priority`. */
105
+ resolvePic(crew: readonly CrewEntry[]): PicResolution;
106
+ /** Returns every constraint violation found in the composition. */
107
+ evaluate(crew: readonly CrewEntry[]): { ok: boolean; violations: Violation[] };
108
+ }
109
+
110
+ /**
111
+ * Build a validator over a specific crew configuration. The validator is
112
+ * pure and stateless beyond the config it closed over; safe to share
113
+ * across components.
114
+ */
115
+ export function createCrewValidator(config: CrewConfig): CrewValidator {
116
+ function attributesOf(role: string): PositionAttributes | undefined {
117
+ return config.positions[role];
118
+ }
119
+
120
+ function rolesFromCrew(crew: readonly CrewEntry[]): string[] {
121
+ return crew.map((c) => c.role).filter((r): r is string => typeof r === "string");
122
+ }
123
+
124
+ function membershipsWithSystemRole(
125
+ crew: readonly CrewEntry[],
126
+ systemRole: string,
127
+ ): readonly CrewEntry[] {
128
+ return crew.filter((c) =>
129
+ c.membership?.roles?.some(
130
+ (r) => r.name === systemRole && (r.company_id == null || r.company_id === null),
131
+ ),
132
+ );
133
+ }
134
+
135
+ function evaluateConstraint(
136
+ constraint: Constraint,
137
+ crew: readonly CrewEntry[],
138
+ ): Violation | null {
139
+ switch (constraint.kind) {
140
+ case "single_pic": {
141
+ if (crew.length === 0) {
142
+ return null;
143
+ }
144
+ const roles = rolesFromCrew(crew);
145
+ for (const eligibleRole of config.pic_priority) {
146
+ const matches = roles.filter((r) => r === eligibleRole).length;
147
+ if (matches === 1) {
148
+ return null;
149
+ }
150
+ if (matches > 1) {
151
+ return {
152
+ kind: constraint.kind,
153
+ message_key: "crew.multiple_same_pic_role",
154
+ details: { role: eligibleRole },
155
+ };
156
+ }
157
+ }
158
+ return {
159
+ kind: constraint.kind,
160
+ message_key: "crew.no_eligible_pic",
161
+ };
162
+ }
163
+
164
+ case "min_members": {
165
+ if (crew.length >= constraint.min) {
166
+ return null;
167
+ }
168
+ return {
169
+ kind: constraint.kind,
170
+ message_key: "crew.min_members",
171
+ details: { min: constraint.min, actual: crew.length },
172
+ };
173
+ }
174
+
175
+ case "when_crew_contains_role_requires_one_of": {
176
+ const roles = rolesFromCrew(crew);
177
+ if (!roles.includes(constraint.when_role)) {
178
+ return null;
179
+ }
180
+ const satisfied = constraint.required_roles.some((r) => roles.includes(r));
181
+ if (satisfied) {
182
+ return null;
183
+ }
184
+ return {
185
+ kind: constraint.kind,
186
+ message_key: constraint.message_key,
187
+ details: {
188
+ when_role: constraint.when_role,
189
+ required_roles: constraint.required_roles,
190
+ },
191
+ };
192
+ }
193
+
194
+ case "when_any_membership_has_system_role_requires_one_of": {
195
+ const flagged = membershipsWithSystemRole(crew, constraint.system_role);
196
+ if (flagged.length === 0) {
197
+ return null;
198
+ }
199
+ const roles = rolesFromCrew(crew);
200
+ const satisfied = constraint.required_roles.some((r) => roles.includes(r));
201
+ if (satisfied) {
202
+ return null;
203
+ }
204
+ return {
205
+ kind: constraint.kind,
206
+ message_key: constraint.message_key,
207
+ details: {
208
+ system_role: constraint.system_role,
209
+ required_roles: constraint.required_roles,
210
+ membership_ids: flagged.map((c) => c.membership_id).filter((id) => id != null),
211
+ },
212
+ };
213
+ }
214
+ }
215
+ }
216
+
217
+ return {
218
+ isOnBoardRole: (role) => attributesOf(role)?.isOnBoard ?? false,
219
+ isInstructorClassRole: (role) => attributesOf(role)?.isInstructorClass ?? false,
220
+ isPicEligibleRole: (role) => attributesOf(role)?.picPriority != null,
221
+
222
+ resolvePic(crew) {
223
+ if (crew.length === 0) {
224
+ return { type: "none" };
225
+ }
226
+ const roles = rolesFromCrew(crew);
227
+ for (const eligibleRole of config.pic_priority) {
228
+ const indexes: number[] = [];
229
+ roles.forEach((r, i) => {
230
+ if (r === eligibleRole) {
231
+ indexes.push(i);
232
+ }
233
+ });
234
+ if (indexes.length === 1) {
235
+ return { type: "ok", index: indexes[0]!, role: eligibleRole };
236
+ }
237
+ if (indexes.length > 1) {
238
+ return { type: "tie", role: eligibleRole };
239
+ }
240
+ }
241
+ return { type: "none" };
242
+ },
243
+
244
+ evaluate(crew) {
245
+ const violations: Violation[] = [];
246
+ for (const constraint of config.constraints) {
247
+ const violation = evaluateConstraint(constraint, crew);
248
+ if (violation !== null) {
249
+ violations.push(violation);
250
+ }
251
+ }
252
+ return { ok: violations.length === 0, violations };
253
+ },
254
+ };
255
+ }
@@ -0,0 +1,70 @@
1
+ import { computed, type ComputedRef, type MaybeRefOrGetter, toValue } from "vue";
2
+
3
+ import { createCrewValidator } from "./index.js";
4
+ import type {
5
+ CrewConfig,
6
+ CrewEntry,
7
+ CrewValidator,
8
+ PicResolution,
9
+ Violation,
10
+ } from "./index.js";
11
+
12
+ export interface UseCrewValidatorResult {
13
+ /** The validator instance (null while config hasn't loaded yet). */
14
+ validator: ComputedRef<CrewValidator | null>;
15
+ /** PIC walk result over the current crew. */
16
+ pic: ComputedRef<PicResolution>;
17
+ /** Every constraint violation in the current crew. */
18
+ violations: ComputedRef<Violation[]>;
19
+ /** True iff PIC resolves cleanly AND no violations are present. */
20
+ isValid: ComputedRef<boolean>;
21
+ /** True iff the role exists in the config and is on-board. */
22
+ isOnBoardRole: (role: string) => boolean;
23
+ /** True iff the role is an instructor variant. */
24
+ isInstructorClassRole: (role: string) => boolean;
25
+ /** True iff the role can hold PIC. */
26
+ isPicEligibleRole: (role: string) => boolean;
27
+ }
28
+
29
+ /**
30
+ * Reactive Vue composable over an SDK `CrewValidator`. Pass the live
31
+ * crew array and the (possibly-loading) crew config; outputs update as
32
+ * either reference changes.
33
+ *
34
+ * `configRef` may be null while the app is fetching `/meta/crew`; the
35
+ * computed properties cleanly degrade to a `none` PIC and empty
36
+ * violations during that window.
37
+ */
38
+ export function useCrewValidator(
39
+ crewRef: MaybeRefOrGetter<readonly CrewEntry[] | null | undefined>,
40
+ configRef: MaybeRefOrGetter<CrewConfig | null | undefined>,
41
+ ): UseCrewValidatorResult {
42
+ const validator = computed<CrewValidator | null>(() => {
43
+ const cfg = toValue(configRef);
44
+ return cfg ? createCrewValidator(cfg) : null;
45
+ });
46
+
47
+ const crew = computed<readonly CrewEntry[]>(() => toValue(crewRef) ?? []);
48
+
49
+ const pic = computed<PicResolution>(() =>
50
+ validator.value ? validator.value.resolvePic(crew.value) : { type: "none" },
51
+ );
52
+
53
+ const violations = computed<Violation[]>(() =>
54
+ validator.value ? validator.value.evaluate(crew.value).violations : [],
55
+ );
56
+
57
+ const isValid = computed<boolean>(
58
+ () => violations.value.length === 0 && pic.value.type === "ok",
59
+ );
60
+
61
+ return {
62
+ validator,
63
+ pic,
64
+ violations,
65
+ isValid,
66
+ isOnBoardRole: (role) => validator.value?.isOnBoardRole(role) ?? false,
67
+ isInstructorClassRole: (role) => validator.value?.isInstructorClassRole(role) ?? false,
68
+ isPicEligibleRole: (role) => validator.value?.isPicEligibleRole(role) ?? false,
69
+ };
70
+ }