gymmonk-schema 0.33.0 → 0.35.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 (49) hide show
  1. package/dist/add-person-form.d.ts +67 -62
  2. package/dist/add-person-form.d.ts.map +1 -1
  3. package/dist/add-person-form.js +95 -44
  4. package/dist/add-person-form.js.map +1 -1
  5. package/dist/center.d.ts +2 -6
  6. package/dist/center.d.ts.map +1 -1
  7. package/dist/center.js +18 -11
  8. package/dist/center.js.map +1 -1
  9. package/dist/check-in.js +2 -2
  10. package/dist/check-in.js.map +1 -1
  11. package/dist/codes.d.ts +121 -0
  12. package/dist/codes.d.ts.map +1 -0
  13. package/dist/codes.js +151 -0
  14. package/dist/codes.js.map +1 -0
  15. package/dist/enrolment.d.ts +100 -0
  16. package/dist/enrolment.d.ts.map +1 -0
  17. package/dist/enrolment.js +112 -0
  18. package/dist/enrolment.js.map +1 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/member-profile.d.ts +26 -26
  24. package/dist/member-profile.d.ts.map +1 -1
  25. package/dist/member-profile.js +37 -45
  26. package/dist/member-profile.js.map +1 -1
  27. package/dist/member.d.ts +14 -10
  28. package/dist/member.d.ts.map +1 -1
  29. package/dist/member.js +11 -9
  30. package/dist/member.js.map +1 -1
  31. package/dist/membership-request.d.ts +6 -1
  32. package/dist/membership-request.d.ts.map +1 -1
  33. package/dist/membership-request.js +8 -2
  34. package/dist/membership-request.js.map +1 -1
  35. package/dist/membership.d.ts +2 -2
  36. package/dist/membership.d.ts.map +1 -1
  37. package/dist/membership.js +15 -3
  38. package/dist/membership.js.map +1 -1
  39. package/dist/onboarding.d.ts.map +1 -1
  40. package/dist/onboarding.js +5 -4
  41. package/dist/onboarding.js.map +1 -1
  42. package/dist/organisation.d.ts +2 -0
  43. package/dist/organisation.d.ts.map +1 -1
  44. package/dist/organisation.js +11 -0
  45. package/dist/organisation.js.map +1 -1
  46. package/dist/staff.d.ts +2 -2
  47. package/dist/staff.js +2 -2
  48. package/dist/staff.js.map +1 -1
  49. package/package.json +1 -1
@@ -32,6 +32,22 @@ import { staffRoleSchema } from './staff.js';
32
32
  */
33
33
  export declare const ADD_PERSON_MIN_AGE_YEARS = 5;
34
34
  export declare const ADD_PERSON_MAX_AGE_YEARS = 100;
35
+ /**
36
+ * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`
37
+ * when nothing was entered.
38
+ *
39
+ * Desk entry does not require a date of birth — a gym adds people with
40
+ * whatever it has to hand — so "blank" is a real answer and has to survive the
41
+ * whole way to the body as an ABSENT field rather than an empty string the
42
+ * API would reject.
43
+ */
44
+ export declare function isoFromDobCells(day: string, month: string, year: string): string | undefined;
45
+ /** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */
46
+ export declare function dobCellsFromIso(iso: string | null | undefined): {
47
+ dobDay: string;
48
+ dobMonth: string;
49
+ dobYear: string;
50
+ };
35
51
  /**
36
52
  * One emergency-contact row: WHO to call, on what number, and how they are
37
53
  * related. A row is either wholly blank (ignored) or names a person.
@@ -65,49 +81,14 @@ export declare const addMemberFormSchema: z.ZodObject<{
65
81
  name: z.ZodString;
66
82
  email: z.ZodEmail;
67
83
  phone: z.ZodString;
68
- dob: z.ZodString;
84
+ dobDay: z.ZodString;
85
+ dobMonth: z.ZodString;
86
+ dobYear: z.ZodString;
69
87
  gender: z.ZodNullable<z.ZodEnum<{
70
88
  male: "male";
71
89
  female: "female";
72
90
  other: "other";
73
91
  }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
74
- address: z.ZodNullable<z.ZodObject<{
75
- country: z.ZodObject<{
76
- id: z.ZodString;
77
- name: z.ZodString;
78
- }, z.core.$strip>;
79
- state: z.ZodObject<{
80
- id: z.ZodString;
81
- name: z.ZodString;
82
- }, z.core.$strip>;
83
- district: z.ZodOptional<z.ZodObject<{
84
- id: z.ZodString;
85
- name: z.ZodString;
86
- }, z.core.$strip>>;
87
- locality: z.ZodOptional<z.ZodObject<{
88
- id: z.ZodString;
89
- name: z.ZodString;
90
- type: z.ZodEnum<{
91
- SUB_DISTRICT: "SUB_DISTRICT";
92
- BLOCK: "BLOCK";
93
- GRAM_PANCHAYAT: "GRAM_PANCHAYAT";
94
- VILLAGE: "VILLAGE";
95
- HAMLET: "HAMLET";
96
- URBAN_BODY: "URBAN_BODY";
97
- URBAN_WARD: "URBAN_WARD";
98
- LOCALITY: "LOCALITY";
99
- }>;
100
- }, z.core.$strip>>;
101
- line1: z.ZodString;
102
- landmark: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
103
- pincode: z.ZodString;
104
- ancestry: z.ZodDefault<z.ZodArray<z.ZodString>>;
105
- coords: z.ZodOptional<z.ZodObject<{
106
- lat: z.ZodNumber;
107
- lng: z.ZodNumber;
108
- source: z.ZodString;
109
- }, z.core.$strip>>;
110
- }, z.core.$strip>>;
111
92
  bloodGroup: z.ZodUnion<[z.ZodEnum<{
112
93
  "A+": "A+";
113
94
  "A-": "A-";
@@ -118,7 +99,6 @@ export declare const addMemberFormSchema: z.ZodObject<{
118
99
  "AB+": "AB+";
119
100
  "AB-": "AB-";
120
101
  }>, z.ZodLiteral<"">]>;
121
- idProofUrl: z.ZodString;
122
102
  avatarUrl: z.ZodString;
123
103
  contacts: z.ZodArray<z.ZodObject<{
124
104
  name: z.ZodString;
@@ -126,19 +106,36 @@ export declare const addMemberFormSchema: z.ZodObject<{
126
106
  phone: z.ZodString;
127
107
  }, z.core.$strip>>;
128
108
  }, z.core.$strip>;
109
+ /**
110
+ * Adding a member asks for the person, not their paperwork.
111
+ *
112
+ * No address and no ID proof: neither is something a front desk has when
113
+ * somebody walks in, both were optional and therefore skipped, and the member's
114
+ * own screens never collected them either. They live on the EDIT form, where an
115
+ * owner fills them in once they actually have them, and on the staff form,
116
+ * where an employer legitimately does.
117
+ */
129
118
  export type AddMemberFormValues = {
130
119
  name: string;
131
120
  email: string;
132
121
  phone: string;
133
- dob: string;
122
+ dobDay: string;
123
+ dobMonth: string;
124
+ dobYear: string;
134
125
  gender: z.infer<typeof genderSchema> | null;
135
- address: Address | null;
136
126
  bloodGroup: z.infer<typeof bloodGroupSchema> | '';
137
- idProofUrl: string;
138
127
  avatarUrl: string;
139
128
  contacts: EmergencyContactRow[];
129
+ /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */
140
130
  trainerId: string;
141
131
  };
132
+ /**
133
+ * Staff keep the address and the ID proof a member is no longer asked for.
134
+ *
135
+ * Not an oversight: this is an employment record. A gym holds an address and
136
+ * an identity document for the people it EMPLOYS for reasons that have nothing
137
+ * to do with why it holds a member's phone number.
138
+ */
142
139
  export declare const addStaffFormSchema: z.ZodObject<{
143
140
  role: z.ZodNullable<z.ZodEnum<{
144
141
  trainer: "trainer";
@@ -146,15 +143,6 @@ export declare const addStaffFormSchema: z.ZodObject<{
146
143
  cleaning: "cleaning";
147
144
  reception: "reception";
148
145
  }>> & z.ZodType<{} | undefined, "trainer" | "manager" | "cleaning" | "reception" | null, z.core.$ZodTypeInternals<{} | undefined, "trainer" | "manager" | "cleaning" | "reception" | null>>;
149
- name: z.ZodString;
150
- email: z.ZodEmail;
151
- phone: z.ZodString;
152
- dob: z.ZodString;
153
- gender: z.ZodNullable<z.ZodEnum<{
154
- male: "male";
155
- female: "female";
156
- other: "other";
157
- }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
158
146
  address: z.ZodNullable<z.ZodObject<{
159
147
  country: z.ZodObject<{
160
148
  id: z.ZodString;
@@ -192,6 +180,18 @@ export declare const addStaffFormSchema: z.ZodObject<{
192
180
  source: z.ZodString;
193
181
  }, z.core.$strip>>;
194
182
  }, z.core.$strip>>;
183
+ idProofUrl: z.ZodString;
184
+ name: z.ZodString;
185
+ email: z.ZodEmail;
186
+ phone: z.ZodString;
187
+ dobDay: z.ZodString;
188
+ dobMonth: z.ZodString;
189
+ dobYear: z.ZodString;
190
+ gender: z.ZodNullable<z.ZodEnum<{
191
+ male: "male";
192
+ female: "female";
193
+ other: "other";
194
+ }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
195
195
  bloodGroup: z.ZodUnion<[z.ZodEnum<{
196
196
  "A+": "A+";
197
197
  "A-": "A-";
@@ -202,7 +202,6 @@ export declare const addStaffFormSchema: z.ZodObject<{
202
202
  "AB+": "AB+";
203
203
  "AB-": "AB-";
204
204
  }>, z.ZodLiteral<"">]>;
205
- idProofUrl: z.ZodString;
206
205
  avatarUrl: z.ZodString;
207
206
  contacts: z.ZodArray<z.ZodObject<{
208
207
  name: z.ZodString;
@@ -212,6 +211,8 @@ export declare const addStaffFormSchema: z.ZodObject<{
212
211
  }, z.core.$strip>;
213
212
  export type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {
214
213
  role: z.infer<typeof staffRoleSchema> | null;
214
+ address: Address | null;
215
+ idProofUrl: string;
215
216
  };
216
217
  /** One blank row so the section renders with a contact to fill. */
217
218
  export declare const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow;
@@ -236,15 +237,6 @@ export declare function toCreateStaffBody(values: AddStaffFormValues): CreateSta
236
237
  export declare const editMemberFormSchema: z.ZodObject<{
237
238
  trainerId: z.ZodString;
238
239
  aadhaarLast4: z.ZodString;
239
- name: z.ZodString;
240
- email: z.ZodEmail;
241
- phone: z.ZodString;
242
- dob: z.ZodString;
243
- gender: z.ZodNullable<z.ZodEnum<{
244
- male: "male";
245
- female: "female";
246
- other: "other";
247
- }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
248
240
  address: z.ZodNullable<z.ZodObject<{
249
241
  country: z.ZodObject<{
250
242
  id: z.ZodString;
@@ -282,6 +274,18 @@ export declare const editMemberFormSchema: z.ZodObject<{
282
274
  source: z.ZodString;
283
275
  }, z.core.$strip>>;
284
276
  }, z.core.$strip>>;
277
+ idProofUrl: z.ZodString;
278
+ name: z.ZodString;
279
+ email: z.ZodEmail;
280
+ phone: z.ZodString;
281
+ dobDay: z.ZodString;
282
+ dobMonth: z.ZodString;
283
+ dobYear: z.ZodString;
284
+ gender: z.ZodNullable<z.ZodEnum<{
285
+ male: "male";
286
+ female: "female";
287
+ other: "other";
288
+ }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
285
289
  bloodGroup: z.ZodUnion<[z.ZodEnum<{
286
290
  "A+": "A+";
287
291
  "A-": "A-";
@@ -292,7 +296,6 @@ export declare const editMemberFormSchema: z.ZodObject<{
292
296
  "AB+": "AB+";
293
297
  "AB-": "AB-";
294
298
  }>, z.ZodLiteral<"">]>;
295
- idProofUrl: z.ZodString;
296
299
  avatarUrl: z.ZodString;
297
300
  contacts: z.ZodArray<z.ZodObject<{
298
301
  name: z.ZodString;
@@ -302,6 +305,8 @@ export declare const editMemberFormSchema: z.ZodObject<{
302
305
  }, z.core.$strip>;
303
306
  export type EditMemberFormValues = AddMemberFormValues & {
304
307
  aadhaarLast4: string;
308
+ address: Address | null;
309
+ idProofUrl: string;
305
310
  };
306
311
  /**
307
312
  * Validated edit form → `PATCH /members/:id` body.
@@ -1 +1 @@
1
- {"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,KAAK,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAS5C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,yBAAyB;;;;iBAuBlC,CAAC;AAEL,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA2ED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY5B,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,GAAG,IAAI,CAAC;IAC5C,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,GAAG,EAAE,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAIF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO3B,CAAC;AAEL,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,GAAG;IACxE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,GAAG,IAAI,CAAC;CAC9C,CAAC;AAgBF,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,EAAE,mBAIrC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,mBAInC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,kBAIlC,CAAC;AAwDF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,WAAW,CAAC,EAAE,oBAAoB,GACjC,gBAAgB,CAMlB;AAED,iDAAiD;AACjD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,eAAe,CAM7E;AAID;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY7B,CAAC;AAEL,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG;IACvD,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,GAAG,gBAAgB,CAYjF"}
1
+ {"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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"}
@@ -32,6 +32,28 @@ import { staffRoleSchema } from './staff.js';
32
32
  */
33
33
  export const ADD_PERSON_MIN_AGE_YEARS = 5;
34
34
  export const ADD_PERSON_MAX_AGE_YEARS = 100;
35
+ // ─── Date of birth, as three cells ───────────────────────────────────────────
36
+ /**
37
+ * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`
38
+ * when nothing was entered.
39
+ *
40
+ * Desk entry does not require a date of birth — a gym adds people with
41
+ * whatever it has to hand — so "blank" is a real answer and has to survive the
42
+ * whole way to the body as an ABSENT field rather than an empty string the
43
+ * API would reject.
44
+ */
45
+ export function isoFromDobCells(day, month, year) {
46
+ if (!day || !month || !year)
47
+ return undefined;
48
+ return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
49
+ }
50
+ /** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */
51
+ export function dobCellsFromIso(iso) {
52
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso ?? '');
53
+ if (!match)
54
+ return { dobDay: '', dobMonth: '', dobYear: '' };
55
+ return { dobYear: match[1], dobMonth: match[2], dobDay: match[3] };
56
+ }
35
57
  // ─── Field-level rules ───────────────────────────────────────────────────────
36
58
  const requiredChoice = (inner, message) => inner.nullable().refine((v) => v !== null, { message });
37
59
  // ─── One emergency-contact row ───────────────────────────────────────────────
@@ -84,20 +106,24 @@ const addPersonBaseShape = {
84
106
  name: z.string().trim().min(1, 'Name is required').max(160),
85
107
  email: z.email('Enter a valid email'),
86
108
  phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),
87
- /** `YYYY-MM-DD` from a native date input; blank when not recorded. */
88
- dob: z.string(),
89
- gender: requiredChoice(genderSchema, 'Select a gender'),
90
109
  /**
91
- * The whole address, or null. Picked through the location drawer, which
92
- * hands back a complete `Address` — area chain, ids, names, ancestry,
93
- * coordinates or nothing. It is therefore atomic: the "half-filled
94
- * address" this schema used to guard against can no longer be expressed.
110
+ * Three cells, not a native date input.
111
+ *
112
+ * `<input type="date">` renders in the BROWSER'S locale, so the same field
113
+ * read "mm/dd/yyyy" at an Indian gym's front desk and a member's own
114
+ * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY
115
+ * cells. Two surfaces collecting one fact in two formats is how a birthday
116
+ * gets typed the wrong way round.
117
+ *
118
+ * All three or none: desk entry does not require a date of birth, so blank
119
+ * is a real answer, but half a date is not.
95
120
  */
96
- address: addressSchema.nullable(),
121
+ dobDay: z.string(),
122
+ dobMonth: z.string(),
123
+ dobYear: z.string(),
124
+ gender: requiredChoice(genderSchema, 'Select a gender'),
97
125
  /** Blank when not recorded. */
98
126
  bloodGroup: bloodGroupSchema.or(z.literal('')),
99
- /** GCS URLs — both upload on pick, so these are never files. */
100
- idProofUrl: z.string(),
101
127
  avatarUrl: z.string(),
102
128
  contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),
103
129
  };
@@ -106,41 +132,34 @@ const addPersonBaseShape = {
106
132
  * a mis-keyed year is the single most common desk-entry slip.
107
133
  */
108
134
  function refineDob(v, ctx) {
109
- if (v.dob === '')
135
+ const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);
136
+ // Nothing entered is fine — the desk records what it has.
137
+ if (!touched)
110
138
  return;
111
- const parts = v.dob.split('-');
112
- const year = Number(parts[0]);
113
- const month = Number(parts[1]);
114
- const day = Number(parts[2]);
115
- if (parts.length !== 3 || !isRealDate(year, month, day)) {
116
- ctx.addIssue({
117
- code: 'custom',
118
- path: ['dob'],
119
- message: 'That date does not exist',
120
- });
139
+ // Every message lands on `dobDay`, the first cell, because the three are one
140
+ // control on screen: an error parked on `dobYear` would sit under a field
141
+ // the form renders as a single row and reads back from `dobDay`.
142
+ if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {
143
+ ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });
144
+ return;
145
+ }
146
+ const day = Number(v.dobDay);
147
+ const month = Number(v.dobMonth);
148
+ const year = Number(v.dobYear);
149
+ if (!isRealDate(year, month, day)) {
150
+ ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });
121
151
  return;
122
152
  }
123
153
  const age = ageInYears(year, month, day);
124
154
  if (age < 0) {
125
155
  ctx.addIssue({
126
156
  code: 'custom',
127
- path: ['dob'],
157
+ path: ['dobDay'],
128
158
  message: 'Date of birth cannot be in the future',
129
159
  });
130
160
  }
131
- else if (age < ADD_PERSON_MIN_AGE_YEARS) {
132
- ctx.addIssue({
133
- code: 'custom',
134
- path: ['dob'],
135
- message: 'Check the year of birth',
136
- });
137
- }
138
- else if (age > ADD_PERSON_MAX_AGE_YEARS) {
139
- ctx.addIssue({
140
- code: 'custom',
141
- path: ['dob'],
142
- message: 'Check the year of birth',
143
- });
161
+ else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {
162
+ ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });
144
163
  }
145
164
  }
146
165
  // ─── Member form ─────────────────────────────────────────────────────────────
@@ -158,10 +177,25 @@ export const addMemberFormSchema = z
158
177
  refineDob(v, ctx);
159
178
  });
160
179
  // ─── Staff form ──────────────────────────────────────────────────────────────
180
+ /**
181
+ * Staff keep the address and the ID proof a member is no longer asked for.
182
+ *
183
+ * Not an oversight: this is an employment record. A gym holds an address and
184
+ * an identity document for the people it EMPLOYS for reasons that have nothing
185
+ * to do with why it holds a member's phone number.
186
+ */
161
187
  export const addStaffFormSchema = z
162
188
  .object({
163
189
  ...addPersonBaseShape,
164
190
  role: requiredChoice(staffRoleSchema, 'Select a staff role'),
191
+ /**
192
+ * The whole address, or null. Picked through the location drawer, which
193
+ * hands back a complete `Address` — area chain, ids, names, ancestry,
194
+ * coordinates — or nothing.
195
+ */
196
+ address: addressSchema.nullable(),
197
+ /** GCS URL — uploads on pick, so this is never a file. */
198
+ idProofUrl: z.string(),
165
199
  })
166
200
  .superRefine((v, ctx) => {
167
201
  refineDob(v, ctx);
@@ -171,11 +205,11 @@ const EMPTY_BASE = {
171
205
  name: '',
172
206
  email: '',
173
207
  phone: '',
174
- dob: '',
208
+ dobDay: '',
209
+ dobMonth: '',
210
+ dobYear: '',
175
211
  gender: null,
176
- address: null,
177
212
  bloodGroup: '',
178
- idProofUrl: '',
179
213
  avatarUrl: '',
180
214
  };
181
215
  /** One blank row so the section renders with a contact to fill. */
@@ -193,6 +227,8 @@ export const EMPTY_ADD_STAFF_FORM = {
193
227
  ...EMPTY_BASE,
194
228
  contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],
195
229
  role: null,
230
+ address: null,
231
+ idProofUrl: '',
196
232
  };
197
233
  // ─── Form → API body ─────────────────────────────────────────────────────────
198
234
  /** One "Name" field on screen, `firstName` + `lastName` on the wire. */
@@ -210,10 +246,11 @@ function orUndefined(value) {
210
246
  function asPhone(value) {
211
247
  return value.replace(/\D/g, '').slice(-10);
212
248
  }
249
+ /**
250
+ * The fields EVERY desk form sends. Address and ID proof are not among them —
251
+ * only two of the three forms ask for those, so each adds its own.
252
+ */
213
253
  function sharedBody(v) {
214
- // Already a complete `Address` or null — the drawer cannot produce anything
215
- // in between, so there is nothing to assemble or re-check here.
216
- const address = v.address ?? undefined;
217
254
  // A row with nothing in it is not a contact. Anything else has already been
218
255
  // through `emergencyContactRowSchema`, which requires a name and a valid
219
256
  // phone the moment a row is touched.
@@ -231,11 +268,11 @@ function sharedBody(v) {
231
268
  phone: asPhone(v.phone),
232
269
  // The form schema guarantees a choice was made before we get here.
233
270
  gender: v.gender ?? 'other',
234
- dob: orUndefined(v.dob),
271
+ // Absent rather than empty when the desk did not record one — see
272
+ // `isoFromDobCells`.
273
+ dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),
235
274
  bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,
236
- address,
237
275
  emergencyContacts,
238
- idProofUrl: orUndefined(v.idProofUrl),
239
276
  avatarUrl: orUndefined(v.avatarUrl),
240
277
  };
241
278
  }
@@ -256,6 +293,10 @@ export function toCreateStaffBody(values) {
256
293
  ...sharedBody(values),
257
294
  // The form schema guarantees a role was chosen.
258
295
  role: values.role ?? 'trainer',
296
+ // Staff-only — see `addStaffFormSchema`. Already a complete `Address` or
297
+ // null; the location drawer cannot produce anything in between.
298
+ address: values.address ?? undefined,
299
+ idProofUrl: orUndefined(values.idProofUrl),
259
300
  };
260
301
  }
261
302
  // ─── Edit member ─────────────────────────────────────────────────────────────
@@ -277,6 +318,16 @@ export const editMemberFormSchema = z
277
318
  .string()
278
319
  .trim()
279
320
  .refine((v) => v === '' || /^\d{4}$/.test(v), 'Enter the last four digits'),
321
+ /**
322
+ * Address and ID proof are asked HERE and not on the add form.
323
+ *
324
+ * Adding somebody happens at a desk with a queue behind them; editing
325
+ * happens later, once the gym actually holds the document. Dropping them
326
+ * from EDIT as well would leave three fields the API stores, the detail
327
+ * screen renders, and no screen anywhere can write.
328
+ */
329
+ address: addressSchema.nullable(),
330
+ idProofUrl: z.string(),
280
331
  })
281
332
  .superRefine((v, ctx) => {
282
333
  refineDob(v, ctx);
@@ -1 +1 @@
1
- {"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { type Address, addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\nimport type { CreateStaffBody } from './staff.js';\nimport { staffRoleSchema } from './staff.js';\n\n// ─── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\n * because a gym also enrols juniors with a guardian present, and the date is\n * typed by staff rather than by the person themselves.\n */\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\n\n// ─── Field-level rules ───────────────────────────────────────────────────────\n\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── One emergency-contact row ───────────────────────────────────────────────\n\n/**\n * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n email: z.email('Enter a valid email'),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /** `YYYY-MM-DD` from a native date input; blank when not recorded. */\n dob: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /**\n * The whole address, or null. Picked through the location drawer, which\n * hands back a complete `Address` — area chain, ids, names, ancestry,\n * coordinates — or nothing. It is therefore atomic: the \"half-filled\n * address\" this schema used to guard against can no longer be expressed.\n */\n address: addressSchema.nullable(),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n /** GCS URLs — both upload on pick, so these are never files. */\n idProofUrl: z.string(),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(v: { dob: string }, ctx: z.RefinementCtx): void {\n if (v.dob === '') return;\n\n const parts = v.dob.split('-');\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n\n if (parts.length !== 3 || !isRealDate(year, month, day)) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'That date does not exist',\n });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n } else if (age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddMemberFormValues = {\n name: string;\n email: string;\n phone: string;\n dob: string;\n gender: z.infer<typeof genderSchema> | null;\n address: Address | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n idProofUrl: string;\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n trainerId: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n email: '',\n phone: '',\n dob: '',\n gender: null,\n address: null,\n bloodGroup: '' as const,\n idProofUrl: '',\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\n // Already a complete `Address` or null — the drawer cannot produce anything\n // in between, so there is nothing to assemble or re-check here.\n const address = v.address ?? undefined;\n\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n email: v.email.trim(),\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n dob: orUndefined(v.dob),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n address,\n emergencyContacts,\n idProofUrl: orUndefined(v.idProofUrl),\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n };\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member reuses the add form's every rule, plus the Aadhaar last-four\n * the desk may not have taken at signup.\n *\n * It is a separate schema rather than the same one because the two forms differ\n * in what \"blank\" means. On the add form a blank optional is simply not\n * provided; on the edit form it is an instruction to CLEAR a value that is\n * already stored, and the mapper has to send `null` rather than omit the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /** Blank when not recorded; exactly four digits otherwise. */\n aadhaarLast4: z\n .string()\n .trim()\n .refine((v) => v === '' || /^\\d{4}$/.test(v), 'Enter the last four digits'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n aadhaarLast4: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The four identity fields go\n * along too; the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n address: values.address,\n idProofUrl: values.idProofUrl.trim(),\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n aadhaarLast4: values.aadhaarLast4.trim() === '' ? null : values.aadhaarLast4.trim(),\n };\n}\n"]}
1
+ {"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { type Address, addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\nimport type { CreateStaffBody } from './staff.js';\nimport { staffRoleSchema } from './staff.js';\n\n// ─── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\n * because a gym also enrols juniors with a guardian present, and the date is\n * typed by staff rather than by the person themselves.\n */\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\n\n// ─── Date of birth, as three cells ───────────────────────────────────────────\n\n/**\n * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`\n * when nothing was entered.\n *\n * Desk entry does not require a date of birth — a gym adds people with\n * whatever it has to hand — so \"blank\" is a real answer and has to survive the\n * whole way to the body as an ABSENT field rather than an empty string the\n * API would reject.\n */\nexport function isoFromDobCells(day: string, month: string, year: string): string | undefined {\n if (!day || !month || !year) return undefined;\n return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;\n}\n\n/** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */\nexport function dobCellsFromIso(iso: string | null | undefined): {\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n} {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso ?? '');\n if (!match) return { dobDay: '', dobMonth: '', dobYear: '' };\n return { dobYear: match[1] as string, dobMonth: match[2] as string, dobDay: match[3] as string };\n}\n\n// ─── Field-level rules ───────────────────────────────────────────────────────\n\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── One emergency-contact row ───────────────────────────────────────────────\n\n/**\n * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n email: z.email('Enter a valid email'),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /**\n * Three cells, not a native date input.\n *\n * `<input type=\"date\">` renders in the BROWSER'S locale, so the same field\n * read \"mm/dd/yyyy\" at an Indian gym's front desk — and a member's own\n * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY\n * cells. Two surfaces collecting one fact in two formats is how a birthday\n * gets typed the wrong way round.\n *\n * All three or none: desk entry does not require a date of birth, so blank\n * is a real answer, but half a date is not.\n */\n dobDay: z.string(),\n dobMonth: z.string(),\n dobYear: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(\n v: { dobDay: string; dobMonth: string; dobYear: string },\n ctx: z.RefinementCtx,\n): void {\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\n // Nothing entered is fine — the desk records what it has.\n if (!touched) return;\n\n // Every message lands on `dobDay`, the first cell, because the three are one\n // control on screen: an error parked on `dobYear` would sit under a field\n // the form renders as a single row and reads back from `dobDay`.\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });\n return;\n }\n\n const day = Number(v.dobDay);\n const month = Number(v.dobMonth);\n const year = Number(v.dobYear);\n\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dobDay'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\n/**\n * Adding a member asks for the person, not their paperwork.\n *\n * No address and no ID proof: neither is something a front desk has when\n * somebody walks in, both were optional and therefore skipped, and the member's\n * own screens never collected them either. They live on the EDIT form, where an\n * owner fills them in once they actually have them, and on the staff form,\n * where an employer legitimately does.\n */\nexport type AddMemberFormValues = {\n name: string;\n email: string;\n phone: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: z.infer<typeof genderSchema> | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */\n trainerId: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\n/**\n * Staff keep the address and the ID proof a member is no longer asked for.\n *\n * Not an oversight: this is an employment record. A gym holds an address and\n * an identity document for the people it EMPLOYS for reasons that have nothing\n * to do with why it holds a member's phone number.\n */\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n /**\n * The whole address, or null. Picked through the location drawer, which\n * hands back a complete `Address` — area chain, ids, names, ancestry,\n * coordinates — or nothing.\n */\n address: addressSchema.nullable(),\n /** GCS URL — uploads on pick, so this is never a file. */\n idProofUrl: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n address: Address | null;\n idProofUrl: string;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n email: '',\n phone: '',\n dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n bloodGroup: '' as const,\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n address: null,\n idProofUrl: '',\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\n/**\n * The fields EVERY desk form sends. Address and ID proof are not among them —\n * only two of the three forms ask for those, so each adds its own.\n */\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n email: v.email.trim(),\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n // Absent rather than empty when the desk did not record one — see\n // `isoFromDobCells`.\n dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n emergencyContacts,\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n // Staff-only — see `addStaffFormSchema`. Already a complete `Address` or\n // null; the location drawer cannot produce anything in between.\n address: values.address ?? undefined,\n idProofUrl: orUndefined(values.idProofUrl),\n };\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member reuses the add form's every rule, plus the Aadhaar last-four\n * the desk may not have taken at signup.\n *\n * It is a separate schema rather than the same one because the two forms differ\n * in what \"blank\" means. On the add form a blank optional is simply not\n * provided; on the edit form it is an instruction to CLEAR a value that is\n * already stored, and the mapper has to send `null` rather than omit the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /** Blank when not recorded; exactly four digits otherwise. */\n aadhaarLast4: z\n .string()\n .trim()\n .refine((v) => v === '' || /^\\d{4}$/.test(v), 'Enter the last four digits'),\n /**\n * Address and ID proof are asked HERE and not on the add form.\n *\n * Adding somebody happens at a desk with a queue behind them; editing\n * happens later, once the gym actually holds the document. Dropping them\n * from EDIT as well would leave three fields the API stores, the detail\n * screen renders, and no screen anywhere can write.\n */\n address: addressSchema.nullable(),\n idProofUrl: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n aadhaarLast4: string;\n address: Address | null;\n idProofUrl: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The four identity fields go\n * along too; the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n address: values.address,\n idProofUrl: values.idProofUrl.trim(),\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n aadhaarLast4: values.aadhaarLast4.trim() === '' ? null : values.aadhaarLast4.trim(),\n };\n}\n"]}
package/dist/center.d.ts CHANGED
@@ -85,12 +85,7 @@ export declare const centerStatusSchema: z.ZodEnum<{
85
85
  inactive: "inactive";
86
86
  }>;
87
87
  export type CenterStatus = z.infer<typeof centerStatusSchema>;
88
- /**
89
- * The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.
90
- * Server-generated and unique; never accepted in a create body.
91
- */
92
- export declare const gymCodeSchema: z.ZodString;
93
- export type GymCode = z.infer<typeof gymCodeSchema>;
88
+ export { type CenterCode, centerCodeSchema } from './codes.js';
94
89
  export declare const createCenterBodySchema: z.ZodObject<{
95
90
  checkInRadiusM: z.ZodOptional<z.ZodNumber>;
96
91
  mapLinkUrl: z.ZodOptional<z.ZodString>;
@@ -336,6 +331,7 @@ export declare const centerSchema: z.ZodObject<{
336
331
  id: z.ZodString;
337
332
  organisationId: z.ZodString;
338
333
  code: z.ZodString;
334
+ sequence: z.ZodNumber;
339
335
  name: z.ZodString;
340
336
  description: z.ZodNullable<z.ZodString>;
341
337
  types: z.ZodArray<z.ZodEnum<{
@@ -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;AAcxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,YAAY,oTAgCf,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCjC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4CvB,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;AAKpE,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;AAexB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,YAAY,oTAgCf,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;AAM9D,OAAO,EAAE,KAAK,UAAU,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAI/D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCjC,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqDvB,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;AAKpE,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}