gymmonk-schema 0.11.0 → 0.13.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 (46) hide show
  1. package/README.md +43 -0
  2. package/dist/center.d.ts +66 -6
  3. package/dist/center.d.ts.map +1 -1
  4. package/dist/center.js +88 -6
  5. package/dist/center.js.map +1 -1
  6. package/dist/equipment.d.ts +49 -0
  7. package/dist/equipment.d.ts.map +1 -1
  8. package/dist/equipment.js +60 -0
  9. package/dist/equipment.js.map +1 -1
  10. package/dist/gym-registration.d.ts +189 -0
  11. package/dist/gym-registration.d.ts.map +1 -0
  12. package/dist/gym-registration.js +37 -0
  13. package/dist/gym-registration.js.map +1 -0
  14. package/dist/index.d.ts +1 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/member-profile.d.ts +1 -1
  19. package/dist/member-profile.d.ts.map +1 -1
  20. package/dist/member-profile.js +10 -1
  21. package/dist/member-profile.js.map +1 -1
  22. package/dist/membership-plan.d.ts +2 -5
  23. package/dist/membership-plan.d.ts.map +1 -1
  24. package/dist/membership-plan.js +4 -4
  25. package/dist/membership-plan.js.map +1 -1
  26. package/dist/messages.d.ts +39 -2
  27. package/dist/messages.d.ts.map +1 -1
  28. package/dist/messages.js +37 -2
  29. package/dist/messages.js.map +1 -1
  30. package/dist/onboarding-form.d.ts +23 -151
  31. package/dist/onboarding-form.d.ts.map +1 -1
  32. package/dist/onboarding-form.js +28 -99
  33. package/dist/onboarding-form.js.map +1 -1
  34. package/dist/onboarding.d.ts +26 -319
  35. package/dist/onboarding.d.ts.map +1 -1
  36. package/dist/onboarding.js +30 -63
  37. package/dist/onboarding.js.map +1 -1
  38. package/dist/organisation.d.ts +138 -0
  39. package/dist/organisation.d.ts.map +1 -1
  40. package/dist/organisation.js +66 -1
  41. package/dist/organisation.js.map +1 -1
  42. package/dist/user.d.ts +40 -0
  43. package/dist/user.d.ts.map +1 -1
  44. package/dist/user.js +43 -0
  45. package/dist/user.js.map +1 -1
  46. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # gymmonk-schema
2
+
3
+ Shared **Zod** schemas, enums and domain types for **GymMonk** (fitness SaaS).
4
+ Single source of truth (SSOT) consumed by both `gymmonk-backend` and
5
+ `gymmonk-web-client`, so validation, wire types and enums never drift between
6
+ the API and the app.
7
+
8
+ ## What's inside
9
+
10
+ - **Common** (`common.ts`) — reusable primitives: `objectIdSchema`,
11
+ `paginationSchema`, `optionalTrimmedString`, `emptyStringToUndefined`.
12
+ - Domain modules (workouts, training library, membership, check-in, …) are
13
+ added here as they land, each re-exported through `src/index.ts`.
14
+
15
+ ## Build & publish (npm-first)
16
+
17
+ ```bash
18
+ npm install
19
+ npm run build # tsc -> dist/
20
+ npm publish --access public
21
+ ```
22
+
23
+ `prepublishOnly` runs `build` + `check` (type-check + biome) as the publish gate.
24
+ `--access public` is a CLI flag, not a manifest field.
25
+
26
+ ## Consume
27
+
28
+ In `gymmonk-backend` and `gymmonk-web-client`:
29
+
30
+ ```bash
31
+ npm install gymmonk-schema@<version>
32
+ # then delete any nested Zod dup so a single Zod instance is used:
33
+ rm -rf node_modules/gymmonk-schema/node_modules
34
+ ```
35
+
36
+ Bump the caret pin in each consumer's `package.json` on every publish (a `0.x`
37
+ caret locks to the same minor, so `npm install` won't auto-pick a new minor).
38
+
39
+ ## Convention
40
+
41
+ Mirrors the jansathi schema family (`dating-schema`, `near-me-schema`) exactly:
42
+ pure ESM, `tsc`-only build, Zod is a **peer** dependency (`^4.4.0`), NodeNext
43
+ `.js` import specifiers, const-object enums fed to `z.enum(...)`.
package/dist/center.d.ts CHANGED
@@ -91,6 +91,7 @@ export declare const createCenterBodySchema: z.ZodObject<{
91
91
  }, z.core.$strip>>;
92
92
  }, z.core.$strip>;
93
93
  floorAreaSqft: z.ZodOptional<z.ZodNumber>;
94
+ capacity: z.ZodOptional<z.ZodNumber>;
94
95
  primaryPhone: z.ZodString;
95
96
  secondaryPhone: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
96
97
  email: z.ZodPreprocess<z.ZodOptional<z.ZodEmail>>;
@@ -190,6 +191,7 @@ export declare const updateCenterBodySchema: z.ZodObject<{
190
191
  }, z.core.$strip>>;
191
192
  }, z.core.$strip>>;
192
193
  floorAreaSqft: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
194
+ capacity: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
193
195
  primaryPhone: z.ZodOptional<z.ZodString>;
194
196
  secondaryPhone: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodString>>>;
195
197
  email: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodEmail>>>;
@@ -292,6 +294,7 @@ export declare const centerSchema: z.ZodObject<{
292
294
  }, z.core.$strip>>;
293
295
  }, z.core.$strip>;
294
296
  floorAreaSqft: z.ZodNullable<z.ZodNumber>;
297
+ capacity: z.ZodNullable<z.ZodNumber>;
295
298
  primaryPhone: z.ZodString;
296
299
  secondaryPhone: z.ZodNullable<z.ZodString>;
297
300
  email: z.ZodNullable<z.ZodString>;
@@ -343,21 +346,78 @@ export declare const centerSchema: z.ZodObject<{
343
346
  updatedAt: z.ZodISODateTime;
344
347
  }, z.core.$strip>;
345
348
  export type Center = z.infer<typeof centerSchema>;
346
- /** A lightweight public directory result for the "find your gym" step. */
349
+ /**
350
+ * The three-up header on a centre's detail screen.
351
+ *
352
+ * Computed, never stored — `activeMembers` counts live memberships and
353
+ * `monthlyRevenueInr` sums this month's successful payments, so the card can
354
+ * never drift from the ledger the way a denormalised total would. `capacity`
355
+ * is the one stored value, and is null until the owner fills it in.
356
+ */
357
+ export declare const centerStatsSchema: z.ZodObject<{
358
+ capacity: z.ZodNullable<z.ZodNumber>;
359
+ activeMembers: z.ZodNumber;
360
+ monthlyRevenueInr: z.ZodNumber;
361
+ }, z.core.$strip>;
362
+ export type CenterStats = z.infer<typeof centerStatsSchema>;
363
+ /** A lightweight public directory result for the "find a gym" screen. */
347
364
  export declare const nearbyGymSchema: z.ZodObject<{
348
365
  id: z.ZodString;
349
366
  code: z.ZodString;
350
367
  name: z.ZodString;
351
368
  area: z.ZodString;
352
- distanceKm: z.ZodNumber;
369
+ district: z.ZodNullable<z.ZodString>;
370
+ state: z.ZodNullable<z.ZodString>;
371
+ distanceKm: z.ZodNullable<z.ZodNumber>;
353
372
  memberCount: z.ZodOptional<z.ZodNumber>;
354
373
  }, z.core.$strip>;
355
374
  export type NearbyGym = z.infer<typeof nearbyGymSchema>;
356
- /** Query params for the nearby-gyms search. */
357
- export declare const nearbyGymsQuerySchema: z.ZodObject<{
375
+ /**
376
+ * The gym directory has THREE ways in, and they differ in how far they reach.
377
+ *
378
+ * `code` exact public gym id — matches anywhere in the country, because
379
+ * a member holding the id already knows which gym they mean and
380
+ * may well be joining one in another state.
381
+ * `q` name search — deliberately narrowed to ONE STATE. Gym names
382
+ * repeat heavily ("Fitness First"), so a nationwide name search
383
+ * returns a list nobody can pick from.
384
+ * `districtId` browse — the gyms around them, nearest first when they share
385
+ * their position.
386
+ *
387
+ * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the
388
+ * address `ancestry` array every center stores.
389
+ */
390
+ export declare const GYM_SEARCH_MAX_RESULTS = 20;
391
+ export declare const gymSearchQuerySchema: z.ZodObject<{
392
+ code: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
393
+ q: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
394
+ stateId: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
395
+ districtId: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
358
396
  lat: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
359
397
  lng: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
360
- q: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
398
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
399
+ }, z.core.$strip>;
400
+ export type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;
401
+ /**
402
+ * The directory response.
403
+ *
404
+ * `truncated` says the district holds more gyms than were returned, which is
405
+ * what the screen turns into "showing the 20 nearest — enter a gym id if yours
406
+ * is not here" rather than letting the member assume their gym is absent.
407
+ */
408
+ export declare const gymSearchResultSchema: z.ZodObject<{
409
+ items: z.ZodArray<z.ZodObject<{
410
+ id: z.ZodString;
411
+ code: z.ZodString;
412
+ name: z.ZodString;
413
+ area: z.ZodString;
414
+ district: z.ZodNullable<z.ZodString>;
415
+ state: z.ZodNullable<z.ZodString>;
416
+ distanceKm: z.ZodNullable<z.ZodNumber>;
417
+ memberCount: z.ZodOptional<z.ZodNumber>;
418
+ }, z.core.$strip>>;
419
+ total: z.ZodNumber;
420
+ truncated: z.ZodBoolean;
361
421
  }, z.core.$strip>;
362
- export type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;
422
+ export type GymSearchResult = z.infer<typeof gymSearchResultSchema>;
363
423
  //# sourceMappingURL=center.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,yDAAyD;AACzD,eAAO,MAAM,YAAY,8IAaf,CAAC;AACX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;EAAuB,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,kBAAkB;;;EAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,aAGwB,CAAC;AACnD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAcjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqBvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD,0EAA0E;AAC1E,eAAO,MAAM,eAAe;;;;;;;iBAO1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,+CAA+C;AAC/C,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
1
+ {"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAaxB,yDAAyD;AACzD,eAAO,MAAM,YAAY,8IAaf,CAAC;AACX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;EAAuB,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,kBAAkB;;;EAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,aAGwB,CAAC;AACnD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuBjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsBvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,yEAAyE;AACzE,eAAO,MAAM,eAAe;;;;;;;;;iBAkB1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,oBAAoB;;;;;;;;iBAwB7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
package/dist/center.js CHANGED
@@ -45,6 +45,15 @@ export const createCenterBodySchema = z.object({
45
45
  address: addressSchema,
46
46
  /** Usable floor area in square feet. */
47
47
  floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),
48
+ /**
49
+ * How many people the branch holds at once.
50
+ *
51
+ * The denominator behind "how full are we" — the centre detail screen shows
52
+ * it beside the active-member count, and crowd-load has no meaning without
53
+ * it. Optional: a gym that has never measured it should not be blocked from
54
+ * saving the rest of its details.
55
+ */
56
+ capacity: z.number().int().min(1).max(100_000).optional(),
48
57
  primaryPhone: phoneSchema,
49
58
  secondaryPhone: optionalPhoneSchema,
50
59
  email: optionalEmailSchema,
@@ -65,6 +74,7 @@ export const centerSchema = z.object({
65
74
  types: z.array(centerTypeSchema),
66
75
  address: addressSchema,
67
76
  floorAreaSqft: z.number().int().nullable(),
77
+ capacity: z.number().int().nullable(),
68
78
  primaryPhone: z.string(),
69
79
  secondaryPhone: z.string().nullable(),
70
80
  email: z.string().nullable(),
@@ -77,20 +87,92 @@ export const centerSchema = z.object({
77
87
  createdAt: isoDateTimeSchema,
78
88
  updatedAt: isoDateTimeSchema,
79
89
  });
80
- // ─── Nearby / directory card ─────────────────────────────────────────────────
81
- /** A lightweight public directory result for the "find your gym" step. */
90
+ // ─── Center stats (owner detail header) ──────────────────────────────────────
91
+ /**
92
+ * The three-up header on a centre's detail screen.
93
+ *
94
+ * Computed, never stored — `activeMembers` counts live memberships and
95
+ * `monthlyRevenueInr` sums this month's successful payments, so the card can
96
+ * never drift from the ledger the way a denormalised total would. `capacity`
97
+ * is the one stored value, and is null until the owner fills it in.
98
+ */
99
+ export const centerStatsSchema = z.object({
100
+ capacity: z.number().int().nullable(),
101
+ activeMembers: z.number().int().nonnegative(),
102
+ monthlyRevenueInr: z.number().nonnegative(),
103
+ });
104
+ // ─── Gym directory (member "find a gym") ─────────────────────────────────────
105
+ /** A lightweight public directory result for the "find a gym" screen. */
82
106
  export const nearbyGymSchema = z.object({
83
107
  id: objectIdSchema,
84
108
  code: gymCodeSchema,
85
109
  name: z.string(),
110
+ /** The narrowest place name we hold (locality → district → state). */
86
111
  area: z.string(),
87
- distanceKm: z.number().nonnegative(),
112
+ district: z.string().nullable(),
113
+ state: z.string().nullable(),
114
+ /**
115
+ * Kilometres from the coordinates the caller supplied.
116
+ *
117
+ * NULL, not 0, when it cannot be computed — the caller sent no position, or
118
+ * the gym's address carries no coordinates. Zero is a real distance ("you are
119
+ * standing in it") and using it as the unknown value put every un-geocoded
120
+ * gym at the top of a nearest-first list.
121
+ */
122
+ distanceKm: z.number().nonnegative().nullable(),
88
123
  memberCount: z.number().int().nonnegative().optional(),
89
124
  });
90
- /** Query params for the nearby-gyms search. */
91
- export const nearbyGymsQuerySchema = z.object({
125
+ /**
126
+ * The gym directory has THREE ways in, and they differ in how far they reach.
127
+ *
128
+ * `code` exact public gym id — matches anywhere in the country, because
129
+ * a member holding the id already knows which gym they mean and
130
+ * may well be joining one in another state.
131
+ * `q` name search — deliberately narrowed to ONE STATE. Gym names
132
+ * repeat heavily ("Fitness First"), so a nationwide name search
133
+ * returns a list nobody can pick from.
134
+ * `districtId` browse — the gyms around them, nearest first when they share
135
+ * their position.
136
+ *
137
+ * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the
138
+ * address `ancestry` array every center stores.
139
+ */
140
+ export const GYM_SEARCH_MAX_RESULTS = 20;
141
+ export const gymSearchQuerySchema = z
142
+ .object({
143
+ /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */
144
+ code: optionalTrimmedString(40),
145
+ /** Name fragment. Requires `stateId`. */
146
+ q: optionalTrimmedString(120),
147
+ stateId: optionalTrimmedString(40),
148
+ districtId: optionalTrimmedString(40),
92
149
  lat: z.coerce.number().min(-90).max(90).optional(),
93
150
  lng: z.coerce.number().min(-180).max(180).optional(),
94
- q: optionalTrimmedString(120),
151
+ limit: z.coerce
152
+ .number()
153
+ .int()
154
+ .min(1)
155
+ .max(GYM_SEARCH_MAX_RESULTS)
156
+ .default(GYM_SEARCH_MAX_RESULTS),
157
+ })
158
+ .refine((v) => !v.q || Boolean(v.stateId), {
159
+ message: 'Pick a state to search gyms by name',
160
+ path: ['stateId'],
161
+ })
162
+ .refine((v) => Boolean(v.code || v.q || v.stateId || v.districtId), {
163
+ message: 'Enter a gym id, or pick where to look',
164
+ path: ['stateId'],
165
+ });
166
+ /**
167
+ * The directory response.
168
+ *
169
+ * `truncated` says the district holds more gyms than were returned, which is
170
+ * what the screen turns into "showing the 20 nearest — enter a gym id if yours
171
+ * is not here" rather than letting the member assume their gym is absent.
172
+ */
173
+ export const gymSearchResultSchema = z.object({
174
+ items: z.array(nearbyGymSchema),
175
+ total: z.number().int().nonnegative(),
176
+ truncated: z.boolean(),
95
177
  });
96
178
  //# sourceMappingURL=center.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,WAAW,GACZ,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAE/D,gFAAgF;AAEhF,yDAAyD;AACzD,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,KAAK;IACL,UAAU;IACV,MAAM;IACN,SAAS;IACT,OAAO;IACP,QAAQ;IACR,UAAU;IACV,KAAK;IACL,QAAQ;IACR,UAAU;IACV,UAAU;IACV,mBAAmB;CACX,CAAC;AACX,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAGrD,gFAAgF;AAEhF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;AAGjE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC;KAC3B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,oBAAoB,EAAE,kBAAkB,CAAC,CAAC;AAGnD,gFAAgF;AAEhF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,yBAAyB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAClE,WAAW,EAAE,qBAAqB,CAAC,IAAI,CAAC;IACxC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,wBAAwB,CAAC;IACjE,OAAO,EAAE,aAAa;IACtB,wCAAwC;IACxC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE;IAChE,YAAY,EAAE,WAAW;IACzB,cAAc,EAAE,mBAAmB;IACnC,KAAK,EAAE,mBAAmB;IAC1B,OAAO,EAAE,qBAAqB,CAAC,GAAG,CAAC;IACnC,KAAK,EAAE,iBAAiB;IACxB,mCAAmC;IACnC,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACjE,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,sBAAsB,GAAG,sBAAsB,CAAC,OAAO,EAAE,CAAC;AAGvE,gFAAgF;AAEhF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,MAAM,CAAC;IACnC,EAAE,EAAE,cAAc;IAClB,cAAc,EAAE,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,aAAa;IACnB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAClC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC;IAChC,OAAO,EAAE,aAAa;IACtB,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAC1C,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE;IACxB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACrC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC9B,KAAK,EAAE,iBAAiB;IACxB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE,kBAAkB;IAC1B,2DAA2D;IAC3D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC3C,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,0EAA0E;AAC1E,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,aAAa;IACnB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE;IACpC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,EAAE;CACvD,CAAC,CAAC;AAGH,+CAA+C;AAC/C,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IAClD,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;IACpD,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC;CAC9B,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/** The activity types a center offers (multi-select). */\nexport const CENTER_TYPES = [\n 'gym',\n 'crossfit',\n 'yoga',\n 'pilates',\n 'zumba',\n 'cardio',\n 'strength',\n 'mma',\n 'boxing',\n 'swimming',\n 'aerobics',\n 'personal-training',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n/**\n * The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.\n * Server-generated and unique; never accepted in a create body.\n */\nexport const gymCodeSchema = z\n .string()\n .trim()\n .regex(/^GYM-\\d{4}-\\d{3,}$/, 'Invalid gym code');\nexport type GymCode = z.infer<typeof gymCodeSchema>;\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one type'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `GYM-YYYY-NNN`. */\n code: gymCodeSchema,\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Nearby / directory card ─────────────────────────────────────────────────\n\n/** A lightweight public directory result for the \"find your gym\" step. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: gymCodeSchema,\n name: z.string(),\n area: z.string(),\n distanceKm: z.number().nonnegative(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/** Query params for the nearby-gyms search. */\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
1
+ {"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/** The activity types a center offers (multi-select). */\nexport const CENTER_TYPES = [\n 'gym',\n 'crossfit',\n 'yoga',\n 'pilates',\n 'zumba',\n 'cardio',\n 'strength',\n 'mma',\n 'boxing',\n 'swimming',\n 'aerobics',\n 'personal-training',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n/**\n * The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.\n * Server-generated and unique; never accepted in a create body.\n */\nexport const gymCodeSchema = z\n .string()\n .trim()\n .regex(/^GYM-\\d{4}-\\d{3,}$/, 'Invalid gym code');\nexport type GymCode = z.infer<typeof gymCodeSchema>;\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one type'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `GYM-YYYY-NNN`. */\n code: gymCodeSchema,\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: gymCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine((v) => Boolean(v.code || v.q || v.stateId || v.districtId), {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n });\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n"]}
@@ -78,6 +78,55 @@ export declare const updateEquipmentBodySchema: z.ZodObject<{
78
78
  notes: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodString>>>;
79
79
  }, z.core.$strip>;
80
80
  export type UpdateEquipmentBody = z.infer<typeof updateEquipmentBodySchema>;
81
+ /**
82
+ * The equipment an owner can add by tapping, per category.
83
+ *
84
+ * A SHARED vocabulary, not a UI list: an owner who types "Treadmill" at one
85
+ * gym and "treadmill" at another produces two rows that never roll up, and the
86
+ * usage analytics that key off equipment name would split in half. The picker
87
+ * offers these; anything genuinely unusual is still addable as a free-text
88
+ * `custom` item, which is what that category is for.
89
+ */
90
+ export declare const EQUIPMENT_CATALOG: Record<EquipmentCategory, readonly string[]>;
91
+ /**
92
+ * Set a center's inventory for a set of items in one call.
93
+ *
94
+ * The add screen is a grid of steppers the owner nudges up and down, and it
95
+ * saves once at the end. Sending each nudge as its own create/update/delete
96
+ * would make a half-saved screen the normal outcome of a flaky connection, so
97
+ * the whole intent goes over as one idempotent statement: after this call,
98
+ * these items are at these quantities. A quantity of ZERO removes the item,
99
+ * which is how the stepper's "back to none" is expressed.
100
+ */
101
+ export declare const equipmentQuantityEntrySchema: z.ZodObject<{
102
+ category: z.ZodEnum<{
103
+ custom: "custom";
104
+ cardio: "cardio";
105
+ strength: "strength";
106
+ weight: "weight";
107
+ training: "training";
108
+ recovery: "recovery";
109
+ }>;
110
+ name: z.ZodString;
111
+ quantity: z.ZodNumber;
112
+ }, z.core.$strip>;
113
+ export type EquipmentQuantityEntry = z.infer<typeof equipmentQuantityEntrySchema>;
114
+ export declare const setEquipmentQuantitiesBodySchema: z.ZodObject<{
115
+ centerId: z.ZodString;
116
+ items: z.ZodArray<z.ZodObject<{
117
+ category: z.ZodEnum<{
118
+ custom: "custom";
119
+ cardio: "cardio";
120
+ strength: "strength";
121
+ weight: "weight";
122
+ training: "training";
123
+ recovery: "recovery";
124
+ }>;
125
+ name: z.ZodString;
126
+ quantity: z.ZodNumber;
127
+ }, z.core.$strip>>;
128
+ }, z.core.$strip>;
129
+ export type SetEquipmentQuantitiesBody = z.infer<typeof setEquipmentQuantitiesBodySchema>;
81
130
  export declare const equipmentQuerySchema: z.ZodObject<{
82
131
  centerId: z.ZodString;
83
132
  category: z.ZodOptional<z.ZodEnum<{
@@ -1 +1 @@
1
- {"version":3,"file":"equipment.d.ts","sourceRoot":"","sources":["../src/equipment.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,oBAAoB,6EAOvB,CAAC;AACX,eAAO,MAAM,uBAAuB;;;;;;;EAA+B,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAIxE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;iBAS1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,4DAA4D;AAC5D,eAAO,MAAM,8BAA8B;;;;;;;;;;;iBAIzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAItF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAMpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;iBAE1B,CAAC;AACb,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E,eAAO,MAAM,oBAAoB;;;;;;;;;;iBAG/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
1
+ {"version":3,"file":"equipment.d.ts","sourceRoot":"","sources":["../src/equipment.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,eAAO,MAAM,oBAAoB,6EAOvB,CAAC;AACX,eAAO,MAAM,uBAAuB;;;;;;;EAA+B,CAAC;AACpE,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAIxE,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;iBAS1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD,4DAA4D;AAC5D,eAAO,MAAM,8BAA8B;;;;;;;;;;;iBAIzC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAItF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;iBAMpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAE5E,eAAO,MAAM,yBAAyB;;;;;;;;;;;;iBAE1B,CAAC;AACb,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CA6B1E,CAAC;AAIF;;;;;;;;;GASG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;iBAIvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;iBAG3C,CAAC;AACH,MAAM,MAAM,0BAA0B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gCAAgC,CAAC,CAAC;AAI1F,eAAO,MAAM,oBAAoB;;;;;;;;;;iBAG/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
package/dist/equipment.js CHANGED
@@ -46,6 +46,66 @@ export const createEquipmentBodySchema = z.object({
46
46
  export const updateEquipmentBodySchema = createEquipmentBodySchema
47
47
  .omit({ centerId: true })
48
48
  .partial();
49
+ // ─── Catalog ─────────────────────────────────────────────────────────────────
50
+ /**
51
+ * The equipment an owner can add by tapping, per category.
52
+ *
53
+ * A SHARED vocabulary, not a UI list: an owner who types "Treadmill" at one
54
+ * gym and "treadmill" at another produces two rows that never roll up, and the
55
+ * usage analytics that key off equipment name would split in half. The picker
56
+ * offers these; anything genuinely unusual is still addable as a free-text
57
+ * `custom` item, which is what that category is for.
58
+ */
59
+ export const EQUIPMENT_CATALOG = {
60
+ cardio: [
61
+ 'Treadmill',
62
+ 'Elliptical',
63
+ 'Stationary Bike',
64
+ 'Rowing Machine',
65
+ 'Stair Climber',
66
+ 'Spin Bike',
67
+ ],
68
+ strength: [
69
+ 'Chest Press',
70
+ 'Lat Pulldown',
71
+ 'Leg Press',
72
+ 'Shoulder Press',
73
+ 'Cable Crossover',
74
+ 'Smith Machine',
75
+ 'Seated Row',
76
+ ],
77
+ weight: ['Dumbbells', 'Barbells', 'Weight Plates', 'Kettlebells', 'Bench Press', 'Squat Rack'],
78
+ training: [
79
+ 'Battle Ropes',
80
+ 'Plyo Box',
81
+ 'Medicine Ball',
82
+ 'TRX Straps',
83
+ 'Agility Ladder',
84
+ 'Punching Bag',
85
+ ],
86
+ recovery: ['Foam Roller', 'Massage Gun', 'Stretching Mat', 'Ice Bath', 'Sauna', 'Steam Room'],
87
+ custom: [],
88
+ };
89
+ // ─── Bulk quantity set (the "Add New Equipment" screen) ──────────────────────
90
+ /**
91
+ * Set a center's inventory for a set of items in one call.
92
+ *
93
+ * The add screen is a grid of steppers the owner nudges up and down, and it
94
+ * saves once at the end. Sending each nudge as its own create/update/delete
95
+ * would make a half-saved screen the normal outcome of a flaky connection, so
96
+ * the whole intent goes over as one idempotent statement: after this call,
97
+ * these items are at these quantities. A quantity of ZERO removes the item,
98
+ * which is how the stepper's "back to none" is expressed.
99
+ */
100
+ export const equipmentQuantityEntrySchema = z.object({
101
+ category: equipmentCategorySchema,
102
+ name: z.string().trim().min(1).max(120),
103
+ quantity: z.number().int().min(0).max(100000),
104
+ });
105
+ export const setEquipmentQuantitiesBodySchema = z.object({
106
+ centerId: objectIdSchema,
107
+ items: z.array(equipmentQuantityEntrySchema).min(1).max(200),
108
+ });
49
109
  // ─── List query ──────────────────────────────────────────────────────────────
50
110
  export const equipmentQuerySchema = z.object({
51
111
  centerId: objectIdSchema,
@@ -1 +1 @@
1
- {"version":3,"file":"equipment.js","sourceRoot":"","sources":["../src/equipment.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEvF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,UAAU;IACV,UAAU;IACV,QAAQ;CACA,CAAC;AACX,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAGpE,gFAAgF;AAEhF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,EAAE,EAAE,cAAc;IAClB,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,uBAAuB;IACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC;IACrD,QAAQ,EAAE,uBAAuB;IACjC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CACtC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,uBAAuB;IACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC3D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,KAAK,EAAE,qBAAqB,CAAC,GAAG,CAAC;CAClC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAyB;KAC/D,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;KACxB,OAAO,EAAE,CAAC;AAGb,gFAAgF;AAEhF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,uBAAuB,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Equipment\n * ==========================\n * A center's equipment inventory. The owner UI groups items by category and\n * shows per-category counts; the entity is a per-item record scoped to a center.\n *\n * @module gymmonk-schema/equipment\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, optionalTrimmedString } from './common.js';\n\n// ─── Category ────────────────────────────────────────────────────────────────\n\nexport const EQUIPMENT_CATEGORIES = [\n 'cardio',\n 'strength',\n 'weight',\n 'training',\n 'recovery',\n 'custom',\n] as const;\nexport const equipmentCategorySchema = z.enum(EQUIPMENT_CATEGORIES);\nexport type EquipmentCategory = z.infer<typeof equipmentCategorySchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const equipmentSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n category: equipmentCategorySchema,\n name: z.string(),\n quantity: z.number().int().nonnegative(),\n notes: z.string().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Equipment = z.infer<typeof equipmentSchema>;\n\n/** Per-category rollup the owner equipment list renders. */\nexport const equipmentCategorySummarySchema = z.object({\n category: equipmentCategorySchema,\n label: z.string(),\n count: z.number().int().nonnegative(),\n});\nexport type EquipmentCategorySummary = z.infer<typeof equipmentCategorySummarySchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\nexport const createEquipmentBodySchema = z.object({\n centerId: objectIdSchema,\n category: equipmentCategorySchema,\n name: z.string().trim().min(1, 'Name is required').max(120),\n quantity: z.number().int().min(1).max(100000).default(1),\n notes: optionalTrimmedString(500),\n});\nexport type CreateEquipmentBody = z.infer<typeof createEquipmentBodySchema>;\n\nexport const updateEquipmentBodySchema = createEquipmentBodySchema\n .omit({ centerId: true })\n .partial();\nexport type UpdateEquipmentBody = z.infer<typeof updateEquipmentBodySchema>;\n\n// ─── List query ──────────────────────────────────────────────────────────────\n\nexport const equipmentQuerySchema = z.object({\n centerId: objectIdSchema,\n category: equipmentCategorySchema.optional(),\n});\nexport type EquipmentQuery = z.infer<typeof equipmentQuerySchema>;\n"]}
1
+ {"version":3,"file":"equipment.js","sourceRoot":"","sources":["../src/equipment.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAEvF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,UAAU;IACV,UAAU;IACV,QAAQ;CACA,CAAC;AACX,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAGpE,gFAAgF;AAEhF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,CAAC;IACtC,EAAE,EAAE,cAAc;IAClB,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,uBAAuB;IACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,CAAC,MAAM,CAAC;IACrD,QAAQ,EAAE,uBAAuB;IACjC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CACtC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChD,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,uBAAuB;IACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC3D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACxD,KAAK,EAAE,qBAAqB,CAAC,GAAG,CAAC;CAClC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,yBAAyB,GAAG,yBAAyB;KAC/D,IAAI,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;KACxB,OAAO,EAAE,CAAC;AAGb,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAiD;IAC7E,MAAM,EAAE;QACN,WAAW;QACX,YAAY;QACZ,iBAAiB;QACjB,gBAAgB;QAChB,eAAe;QACf,WAAW;KACZ;IACD,QAAQ,EAAE;QACR,aAAa;QACb,cAAc;QACd,WAAW;QACX,gBAAgB;QAChB,iBAAiB;QACjB,eAAe;QACf,YAAY;KACb;IACD,MAAM,EAAE,CAAC,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,CAAC;IAC9F,QAAQ,EAAE;QACR,cAAc;QACd,UAAU;QACV,eAAe;QACf,YAAY;QACZ,gBAAgB;QAChB,cAAc;KACf;IACD,QAAQ,EAAE,CAAC,aAAa,EAAE,aAAa,EAAE,gBAAgB,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,CAAC;IAC7F,MAAM,EAAE,EAAE;CACX,CAAC;AAEF,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,QAAQ,EAAE,uBAAuB;IACjC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IACvC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC;CAC9C,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gCAAgC,GAAG,CAAC,CAAC,MAAM,CAAC;IACvD,QAAQ,EAAE,cAAc;IACxB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;CAC7D,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,uBAAuB,CAAC,QAAQ,EAAE;CAC7C,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Equipment\n * ==========================\n * A center's equipment inventory. The owner UI groups items by category and\n * shows per-category counts; the entity is a per-item record scoped to a center.\n *\n * @module gymmonk-schema/equipment\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, optionalTrimmedString } from './common.js';\n\n// ─── Category ────────────────────────────────────────────────────────────────\n\nexport const EQUIPMENT_CATEGORIES = [\n 'cardio',\n 'strength',\n 'weight',\n 'training',\n 'recovery',\n 'custom',\n] as const;\nexport const equipmentCategorySchema = z.enum(EQUIPMENT_CATEGORIES);\nexport type EquipmentCategory = z.infer<typeof equipmentCategorySchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const equipmentSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n category: equipmentCategorySchema,\n name: z.string(),\n quantity: z.number().int().nonnegative(),\n notes: z.string().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Equipment = z.infer<typeof equipmentSchema>;\n\n/** Per-category rollup the owner equipment list renders. */\nexport const equipmentCategorySummarySchema = z.object({\n category: equipmentCategorySchema,\n label: z.string(),\n count: z.number().int().nonnegative(),\n});\nexport type EquipmentCategorySummary = z.infer<typeof equipmentCategorySummarySchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\nexport const createEquipmentBodySchema = z.object({\n centerId: objectIdSchema,\n category: equipmentCategorySchema,\n name: z.string().trim().min(1, 'Name is required').max(120),\n quantity: z.number().int().min(1).max(100000).default(1),\n notes: optionalTrimmedString(500),\n});\nexport type CreateEquipmentBody = z.infer<typeof createEquipmentBodySchema>;\n\nexport const updateEquipmentBodySchema = createEquipmentBodySchema\n .omit({ centerId: true })\n .partial();\nexport type UpdateEquipmentBody = z.infer<typeof updateEquipmentBodySchema>;\n\n// ─── Catalog ─────────────────────────────────────────────────────────────────\n\n/**\n * The equipment an owner can add by tapping, per category.\n *\n * A SHARED vocabulary, not a UI list: an owner who types \"Treadmill\" at one\n * gym and \"treadmill\" at another produces two rows that never roll up, and the\n * usage analytics that key off equipment name would split in half. The picker\n * offers these; anything genuinely unusual is still addable as a free-text\n * `custom` item, which is what that category is for.\n */\nexport const EQUIPMENT_CATALOG: Record<EquipmentCategory, readonly string[]> = {\n cardio: [\n 'Treadmill',\n 'Elliptical',\n 'Stationary Bike',\n 'Rowing Machine',\n 'Stair Climber',\n 'Spin Bike',\n ],\n strength: [\n 'Chest Press',\n 'Lat Pulldown',\n 'Leg Press',\n 'Shoulder Press',\n 'Cable Crossover',\n 'Smith Machine',\n 'Seated Row',\n ],\n weight: ['Dumbbells', 'Barbells', 'Weight Plates', 'Kettlebells', 'Bench Press', 'Squat Rack'],\n training: [\n 'Battle Ropes',\n 'Plyo Box',\n 'Medicine Ball',\n 'TRX Straps',\n 'Agility Ladder',\n 'Punching Bag',\n ],\n recovery: ['Foam Roller', 'Massage Gun', 'Stretching Mat', 'Ice Bath', 'Sauna', 'Steam Room'],\n custom: [],\n};\n\n// ─── Bulk quantity set (the \"Add New Equipment\" screen) ──────────────────────\n\n/**\n * Set a center's inventory for a set of items in one call.\n *\n * The add screen is a grid of steppers the owner nudges up and down, and it\n * saves once at the end. Sending each nudge as its own create/update/delete\n * would make a half-saved screen the normal outcome of a flaky connection, so\n * the whole intent goes over as one idempotent statement: after this call,\n * these items are at these quantities. A quantity of ZERO removes the item,\n * which is how the stepper's \"back to none\" is expressed.\n */\nexport const equipmentQuantityEntrySchema = z.object({\n category: equipmentCategorySchema,\n name: z.string().trim().min(1).max(120),\n quantity: z.number().int().min(0).max(100000),\n});\nexport type EquipmentQuantityEntry = z.infer<typeof equipmentQuantityEntrySchema>;\n\nexport const setEquipmentQuantitiesBodySchema = z.object({\n centerId: objectIdSchema,\n items: z.array(equipmentQuantityEntrySchema).min(1).max(200),\n});\nexport type SetEquipmentQuantitiesBody = z.infer<typeof setEquipmentQuantitiesBodySchema>;\n\n// ─── List query ──────────────────────────────────────────────────────────────\n\nexport const equipmentQuerySchema = z.object({\n centerId: objectIdSchema,\n category: equipmentCategorySchema.optional(),\n});\nexport type EquipmentQuery = z.infer<typeof equipmentQuerySchema>;\n"]}