gymmonk-schema 0.7.0 → 0.8.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.
@@ -8,9 +8,9 @@
8
8
  * Why these live here rather than in the client, and why they are distinct from
9
9
  * `createMemberBodySchema`: the form's working shape is not the wire shape. It
10
10
  * holds ONE "name" field the API splits in two, `null` for a choice not yet
11
- * made, `''` for an optional value left blank, and an address whose four cells
12
- * are filled one at a time. Those are legitimate mid-edit states that the API
13
- * body must never see. Expressing the same rules over the form shape here keeps
11
+ * made, and `''` for an optional value left blank. Those are legitimate mid-edit
12
+ * states that the API body must never see. Expressing the same rules over the
13
+ * form shape here keeps
14
14
  * every message, bound and regex defined once — the client cannot drift from
15
15
  * what the server will accept, and the coercion from one shape to the other is
16
16
  * part of the contract instead of ad-hoc code in a component.
@@ -22,7 +22,7 @@
22
22
  import { z } from 'zod';
23
23
  import type { CreateMemberBody } from './member.js';
24
24
  import type { AssignMembershipBody } from './membership.js';
25
- import { bloodGroupSchema, genderSchema, INDIAN_STATES } from './shared.js';
25
+ import { type Address, bloodGroupSchema, genderSchema } from './shared.js';
26
26
  import type { CreateStaffBody } from './staff.js';
27
27
  import { staffRoleSchema } from './staff.js';
28
28
  /**
@@ -58,47 +58,43 @@ export declare const addMemberFormSchema: z.ZodObject<{
58
58
  female: "female";
59
59
  other: "other";
60
60
  }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
61
- street: z.ZodString;
62
- city: z.ZodString;
63
- state: z.ZodNullable<z.ZodEnum<{
64
- "Andhra Pradesh": "Andhra Pradesh";
65
- "Arunachal Pradesh": "Arunachal Pradesh";
66
- Assam: "Assam";
67
- Bihar: "Bihar";
68
- Chhattisgarh: "Chhattisgarh";
69
- Goa: "Goa";
70
- Gujarat: "Gujarat";
71
- Haryana: "Haryana";
72
- "Himachal Pradesh": "Himachal Pradesh";
73
- Jharkhand: "Jharkhand";
74
- Karnataka: "Karnataka";
75
- Kerala: "Kerala";
76
- "Madhya Pradesh": "Madhya Pradesh";
77
- Maharashtra: "Maharashtra";
78
- Manipur: "Manipur";
79
- Meghalaya: "Meghalaya";
80
- Mizoram: "Mizoram";
81
- Nagaland: "Nagaland";
82
- Odisha: "Odisha";
83
- Punjab: "Punjab";
84
- Rajasthan: "Rajasthan";
85
- Sikkim: "Sikkim";
86
- "Tamil Nadu": "Tamil Nadu";
87
- Telangana: "Telangana";
88
- Tripura: "Tripura";
89
- "Uttar Pradesh": "Uttar Pradesh";
90
- Uttarakhand: "Uttarakhand";
91
- "West Bengal": "West Bengal";
92
- "Andaman and Nicobar Islands": "Andaman and Nicobar Islands";
93
- Chandigarh: "Chandigarh";
94
- "Dadra and Nagar Haveli and Daman and Diu": "Dadra and Nagar Haveli and Daman and Diu";
95
- Delhi: "Delhi";
96
- "Jammu and Kashmir": "Jammu and Kashmir";
97
- Ladakh: "Ladakh";
98
- Lakshadweep: "Lakshadweep";
99
- Puducherry: "Puducherry";
100
- }>>;
101
- pincode: z.ZodString;
61
+ address: z.ZodNullable<z.ZodObject<{
62
+ country: z.ZodObject<{
63
+ id: z.ZodString;
64
+ name: z.ZodString;
65
+ }, z.core.$strip>;
66
+ state: z.ZodObject<{
67
+ id: z.ZodString;
68
+ name: z.ZodString;
69
+ }, z.core.$strip>;
70
+ district: z.ZodOptional<z.ZodObject<{
71
+ id: z.ZodString;
72
+ name: z.ZodString;
73
+ }, z.core.$strip>>;
74
+ locality: z.ZodOptional<z.ZodObject<{
75
+ id: z.ZodString;
76
+ name: z.ZodString;
77
+ type: z.ZodEnum<{
78
+ SUB_DISTRICT: "SUB_DISTRICT";
79
+ BLOCK: "BLOCK";
80
+ GRAM_PANCHAYAT: "GRAM_PANCHAYAT";
81
+ VILLAGE: "VILLAGE";
82
+ HAMLET: "HAMLET";
83
+ URBAN_BODY: "URBAN_BODY";
84
+ URBAN_WARD: "URBAN_WARD";
85
+ LOCALITY: "LOCALITY";
86
+ }>;
87
+ }, z.core.$strip>>;
88
+ line1: z.ZodString;
89
+ landmark: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
90
+ pincode: z.ZodString;
91
+ ancestry: z.ZodDefault<z.ZodArray<z.ZodString>>;
92
+ coords: z.ZodOptional<z.ZodObject<{
93
+ lat: z.ZodNumber;
94
+ lng: z.ZodNumber;
95
+ source: z.ZodString;
96
+ }, z.core.$strip>>;
97
+ }, z.core.$strip>>;
102
98
  bloodGroup: z.ZodUnion<[z.ZodEnum<{
103
99
  "A+": "A+";
104
100
  "A-": "A-";
@@ -122,10 +118,7 @@ export type AddMemberFormValues = {
122
118
  phone: string;
123
119
  dob: string;
124
120
  gender: z.infer<typeof genderSchema> | null;
125
- street: string;
126
- city: string;
127
- state: (typeof INDIAN_STATES)[number] | null;
128
- pincode: string;
121
+ address: Address | null;
129
122
  bloodGroup: z.infer<typeof bloodGroupSchema> | '';
130
123
  idProofUrl: string;
131
124
  avatarUrl: string;
@@ -148,47 +141,43 @@ export declare const addStaffFormSchema: z.ZodObject<{
148
141
  female: "female";
149
142
  other: "other";
150
143
  }>> & z.ZodType<{} | undefined, "male" | "female" | "other" | null, z.core.$ZodTypeInternals<{} | undefined, "male" | "female" | "other" | null>>;
151
- street: z.ZodString;
152
- city: z.ZodString;
153
- state: z.ZodNullable<z.ZodEnum<{
154
- "Andhra Pradesh": "Andhra Pradesh";
155
- "Arunachal Pradesh": "Arunachal Pradesh";
156
- Assam: "Assam";
157
- Bihar: "Bihar";
158
- Chhattisgarh: "Chhattisgarh";
159
- Goa: "Goa";
160
- Gujarat: "Gujarat";
161
- Haryana: "Haryana";
162
- "Himachal Pradesh": "Himachal Pradesh";
163
- Jharkhand: "Jharkhand";
164
- Karnataka: "Karnataka";
165
- Kerala: "Kerala";
166
- "Madhya Pradesh": "Madhya Pradesh";
167
- Maharashtra: "Maharashtra";
168
- Manipur: "Manipur";
169
- Meghalaya: "Meghalaya";
170
- Mizoram: "Mizoram";
171
- Nagaland: "Nagaland";
172
- Odisha: "Odisha";
173
- Punjab: "Punjab";
174
- Rajasthan: "Rajasthan";
175
- Sikkim: "Sikkim";
176
- "Tamil Nadu": "Tamil Nadu";
177
- Telangana: "Telangana";
178
- Tripura: "Tripura";
179
- "Uttar Pradesh": "Uttar Pradesh";
180
- Uttarakhand: "Uttarakhand";
181
- "West Bengal": "West Bengal";
182
- "Andaman and Nicobar Islands": "Andaman and Nicobar Islands";
183
- Chandigarh: "Chandigarh";
184
- "Dadra and Nagar Haveli and Daman and Diu": "Dadra and Nagar Haveli and Daman and Diu";
185
- Delhi: "Delhi";
186
- "Jammu and Kashmir": "Jammu and Kashmir";
187
- Ladakh: "Ladakh";
188
- Lakshadweep: "Lakshadweep";
189
- Puducherry: "Puducherry";
190
- }>>;
191
- pincode: z.ZodString;
144
+ address: z.ZodNullable<z.ZodObject<{
145
+ country: z.ZodObject<{
146
+ id: z.ZodString;
147
+ name: z.ZodString;
148
+ }, z.core.$strip>;
149
+ state: z.ZodObject<{
150
+ id: z.ZodString;
151
+ name: z.ZodString;
152
+ }, z.core.$strip>;
153
+ district: z.ZodOptional<z.ZodObject<{
154
+ id: z.ZodString;
155
+ name: z.ZodString;
156
+ }, z.core.$strip>>;
157
+ locality: z.ZodOptional<z.ZodObject<{
158
+ id: z.ZodString;
159
+ name: z.ZodString;
160
+ type: z.ZodEnum<{
161
+ SUB_DISTRICT: "SUB_DISTRICT";
162
+ BLOCK: "BLOCK";
163
+ GRAM_PANCHAYAT: "GRAM_PANCHAYAT";
164
+ VILLAGE: "VILLAGE";
165
+ HAMLET: "HAMLET";
166
+ URBAN_BODY: "URBAN_BODY";
167
+ URBAN_WARD: "URBAN_WARD";
168
+ LOCALITY: "LOCALITY";
169
+ }>;
170
+ }, z.core.$strip>>;
171
+ line1: z.ZodString;
172
+ landmark: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
173
+ pincode: z.ZodString;
174
+ ancestry: z.ZodDefault<z.ZodArray<z.ZodString>>;
175
+ coords: z.ZodOptional<z.ZodObject<{
176
+ lat: z.ZodNumber;
177
+ lng: z.ZodNumber;
178
+ source: z.ZodString;
179
+ }, z.core.$strip>>;
180
+ }, z.core.$strip>>;
192
181
  bloodGroup: z.ZodUnion<[z.ZodEnum<{
193
182
  "A+": "A+";
194
183
  "A-": "A-";
@@ -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;AAGxB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3F,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;AAE5C,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAcxC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;;iBAkBlC,CAAC;AAEL,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA0FD,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAa5B,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,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAQ3B,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;AAmBF,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,EAAE,mBAAqD,CAAC;AAE5F,eAAO,MAAM,qBAAqB,EAAE,mBAInC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,kBAIlC,CAAC;AA4DF;;;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"}
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,MAAM,aAAa,CAAC;AAEpD,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;AAE5C,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,IAAI,CAAC;AASxC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB;;;iBAkBlC,CAAC;AAEL,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA+DD,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,mBAAqD,CAAC;AAE5F,eAAO,MAAM,qBAAqB,EAAE,mBAInC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,kBAIlC,CAAC;AAsDF;;;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"}
@@ -8,9 +8,9 @@
8
8
  * Why these live here rather than in the client, and why they are distinct from
9
9
  * `createMemberBodySchema`: the form's working shape is not the wire shape. It
10
10
  * holds ONE "name" field the API splits in two, `null` for a choice not yet
11
- * made, `''` for an optional value left blank, and an address whose four cells
12
- * are filled one at a time. Those are legitimate mid-edit states that the API
13
- * body must never see. Expressing the same rules over the form shape here keeps
11
+ * made, and `''` for an optional value left blank. Those are legitimate mid-edit
12
+ * states that the API body must never see. Expressing the same rules over the
13
+ * form shape here keeps
14
14
  * every message, bound and regex defined once — the client cannot drift from
15
15
  * what the server will accept, and the coercion from one shape to the other is
16
16
  * part of the contract instead of ad-hoc code in a component.
@@ -20,9 +20,9 @@
20
20
  * @module gymmonk-schema/add-person-form
21
21
  */
22
22
  import { z } from 'zod';
23
- import { ageInYears, isRealDate, PHONE_REGEX, PINCODE_REGEX } from './common.js';
23
+ import { ageInYears, isRealDate, PHONE_REGEX } from './common.js';
24
24
  import { emergencyContactSchema } from './member-profile.js';
25
- import { addressSchema, bloodGroupSchema, genderSchema, INDIAN_STATES } from './shared.js';
25
+ import { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';
26
26
  import { staffRoleSchema } from './staff.js';
27
27
  // ─── Bounds ──────────────────────────────────────────────────────────────────
28
28
  /**
@@ -36,10 +36,6 @@ export const ADD_PERSON_MAX_AGE_YEARS = 100;
36
36
  export const MAX_EMERGENCY_CONTACTS = 5;
37
37
  // ─── Field-level rules ───────────────────────────────────────────────────────
38
38
  const requiredChoice = (inner, message) => inner.nullable().refine((v) => v !== null, { message });
39
- /** Blank is allowed; anything else must be a valid 10-digit mobile. */
40
- const optionalFormPhone = z
41
- .string()
42
- .refine((v) => v === '' || PHONE_REGEX.test(v), 'Enter a valid 10-digit mobile number');
43
39
  // ─── One emergency-contact row ───────────────────────────────────────────────
44
40
  /**
45
41
  * The design collects a RELATIONSHIP and a phone, not a name — so a row is
@@ -75,18 +71,13 @@ const addPersonBaseShape = {
75
71
  /** `YYYY-MM-DD` from a native date input; blank when not recorded. */
76
72
  dob: z.string(),
77
73
  gender: requiredChoice(genderSchema, 'Select a gender'),
78
- // Address — optional as a whole, all-or-nothing when started. See the refine.
79
- street: z.string().trim().max(200),
80
- city: z.string().trim().max(100),
81
74
  /**
82
- * Null until picked in the area drawer. Carries its OWN message: the bare
83
- * enum failure renders all 36 states into the field error, which is unusable
84
- * on a phone. The value can only miss when the reference data hands back a
85
- * name outside the canonical set, and "pick from the list" is the right
86
- * instruction either way.
75
+ * The whole address, or null. Picked through the location drawer, which
76
+ * hands back a complete `Address` area chain, ids, names, ancestry,
77
+ * coordinates or nothing. It is therefore atomic: the "half-filled
78
+ * address" this schema used to guard against can no longer be expressed.
87
79
  */
88
- state: z.enum(INDIAN_STATES, { error: 'Select a state from the list' }).nullable(),
89
- pincode: z.string().trim(),
80
+ address: addressSchema.nullable(),
90
81
  /** Blank when not recorded. */
91
82
  bloodGroup: bloodGroupSchema.or(z.literal('')),
92
83
  /** GCS URLs — both upload on pick, so these are never files. */
@@ -111,7 +102,11 @@ function refineDob(v, ctx) {
111
102
  }
112
103
  const age = ageInYears(year, month, day);
113
104
  if (age < 0) {
114
- ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Date of birth cannot be in the future' });
105
+ ctx.addIssue({
106
+ code: 'custom',
107
+ path: ['dob'],
108
+ message: 'Date of birth cannot be in the future',
109
+ });
115
110
  }
116
111
  else if (age < ADD_PERSON_MIN_AGE_YEARS) {
117
112
  ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Check the year of birth' });
@@ -120,28 +115,6 @@ function refineDob(v, ctx) {
120
115
  ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Check the year of birth' });
121
116
  }
122
117
  }
123
- /**
124
- * The address is optional, but a HALF-filled one is not — silently dropping it
125
- * (which is what the client used to do) loses data the owner believed they had
126
- * saved. Once any cell is touched, all four must be valid.
127
- */
128
- function refineAddress(v, ctx) {
129
- const started = Boolean(v.street || v.city || v.state || v.pincode);
130
- if (!started)
131
- return;
132
- if (!v.street) {
133
- ctx.addIssue({ code: 'custom', path: ['street'], message: 'Street address is required' });
134
- }
135
- if (!v.city) {
136
- ctx.addIssue({ code: 'custom', path: ['city'], message: 'City is required' });
137
- }
138
- if (!v.state) {
139
- ctx.addIssue({ code: 'custom', path: ['state'], message: 'Select a state' });
140
- }
141
- if (!PINCODE_REGEX.test(v.pincode)) {
142
- ctx.addIssue({ code: 'custom', path: ['pincode'], message: 'Enter a valid 6-digit pincode' });
143
- }
144
- }
145
118
  // ─── Member form ─────────────────────────────────────────────────────────────
146
119
  export const addMemberFormSchema = z
147
120
  .object({
@@ -155,7 +128,6 @@ export const addMemberFormSchema = z
155
128
  })
156
129
  .superRefine((v, ctx) => {
157
130
  refineDob(v, ctx);
158
- refineAddress(v, ctx);
159
131
  });
160
132
  // ─── Staff form ──────────────────────────────────────────────────────────────
161
133
  export const addStaffFormSchema = z
@@ -165,7 +137,6 @@ export const addStaffFormSchema = z
165
137
  })
166
138
  .superRefine((v, ctx) => {
167
139
  refineDob(v, ctx);
168
- refineAddress(v, ctx);
169
140
  });
170
141
  // ─── Empty states ────────────────────────────────────────────────────────────
171
142
  const EMPTY_BASE = {
@@ -174,10 +145,7 @@ const EMPTY_BASE = {
174
145
  phone: '',
175
146
  dob: '',
176
147
  gender: null,
177
- street: '',
178
- city: '',
179
- state: null,
180
- pincode: '',
148
+ address: null,
181
149
  bloodGroup: '',
182
150
  idProofUrl: '',
183
151
  avatarUrl: '',
@@ -211,14 +179,9 @@ function asPhone(value) {
211
179
  return value.replace(/\D/g, '').slice(-10);
212
180
  }
213
181
  function sharedBody(v) {
214
- const address = v.street && v.city && v.state && PINCODE_REGEX.test(v.pincode)
215
- ? addressSchema.parse({
216
- street: v.street,
217
- city: v.city,
218
- state: v.state,
219
- pincode: v.pincode.trim(),
220
- })
221
- : undefined;
182
+ // Already a complete `Address` or null the drawer cannot produce anything
183
+ // in between, so there is nothing to assemble or re-check here.
184
+ const address = v.address ?? undefined;
222
185
  const emergencyContacts = v.contacts
223
186
  .filter((c) => c.relationship !== '' || c.phone !== '')
224
187
  .map((c) => emergencyContactSchema.parse({
@@ -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, `''` for an optional value left blank, and an address whose four cells\n * are filled one at a time. Those are legitimate mid-edit states that the API\n * body must never see. Expressing the same rules over the 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, PINCODE_REGEX } from './common.js';\nimport { emergencyContactSchema } from './member-profile.js';\nimport type { CreateMemberBody } from './member.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { addressSchema, bloodGroupSchema, genderSchema, INDIAN_STATES } 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/** Matches `createMemberBodySchema.emergencyContacts`. */\nexport const MAX_EMERGENCY_CONTACTS = 5;\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/** Blank is allowed; anything else must be a valid 10-digit mobile. */\nconst optionalFormPhone = z\n .string()\n .refine((v) => v === '' || PHONE_REGEX.test(v), 'Enter a valid 10-digit mobile number');\n\n// ─── One emergency-contact row ───────────────────────────────────────────────\n\n/**\n * The design collects a RELATIONSHIP and a phone, not a name — so a row is\n * either wholly blank (ignored) or has both. `name` is synthesised from the\n * relationship in the mapper, since `emergencyContactSchema` requires one.\n */\nexport const emergencyContactRowSchema = z\n .object({\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.relationship === '') {\n ctx.addIssue({ code: 'custom', path: ['relationship'], message: 'Add the relationship' });\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 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 // Address — optional as a whole, all-or-nothing when started. See the refine.\n street: z.string().trim().max(200),\n city: z.string().trim().max(100),\n /**\n * Null until picked in the area drawer. Carries its OWN message: the bare\n * enum failure renders all 36 states into the field error, which is unusable\n * on a phone. The value can only miss when the reference data hands back a\n * name outside the canonical set, and \"pick from the list\" is the right\n * instruction either way.\n */\n state: z.enum(INDIAN_STATES, { error: 'Select a state from the list' }).nullable(),\n pincode: z.string().trim(),\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({ code: 'custom', path: ['dob'], message: 'That date does not exist' });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Date of birth cannot be in the future' });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Check the year of birth' });\n } else if (age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Check the year of birth' });\n }\n}\n\n/**\n * The address is optional, but a HALF-filled one is not — silently dropping it\n * (which is what the client used to do) loses data the owner believed they had\n * saved. Once any cell is touched, all four must be valid.\n */\nfunction refineAddress(\n v: { street: string; city: string; state: string | null; pincode: string },\n ctx: z.RefinementCtx,\n): void {\n const started = Boolean(v.street || v.city || v.state || v.pincode);\n if (!started) return;\n\n if (!v.street) {\n ctx.addIssue({ code: 'custom', path: ['street'], message: 'Street address is required' });\n }\n if (!v.city) {\n ctx.addIssue({ code: 'custom', path: ['city'], message: 'City is required' });\n }\n if (!v.state) {\n ctx.addIssue({ code: 'custom', path: ['state'], message: 'Select a state' });\n }\n if (!PINCODE_REGEX.test(v.pincode)) {\n ctx.addIssue({ code: 'custom', path: ['pincode'], message: 'Enter a valid 6-digit pincode' });\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 refineAddress(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 street: string;\n city: string;\n state: (typeof INDIAN_STATES)[number] | null;\n pincode: string;\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 refineAddress(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 street: '',\n city: '',\n state: null,\n pincode: '',\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 = { relationship: '', phone: '' };\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 const address =\n v.street && v.city && v.state && PINCODE_REGEX.test(v.pincode)\n ? addressSchema.parse({\n street: v.street,\n city: v.city,\n state: v.state,\n pincode: v.pincode.trim(),\n })\n : undefined;\n\n const emergencyContacts = v.contacts\n .filter((c) => c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n // The form captures a relationship, not a name; it doubles as the label.\n name: c.relationship.trim(),\n phone: asPhone(c.phone),\n relationship: c.relationship.trim(),\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"]}
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 } from './member.js';\nimport { emergencyContactSchema } 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/** Matches `createMemberBodySchema.emergencyContacts`. */\nexport const MAX_EMERGENCY_CONTACTS = 5;\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 * The design collects a RELATIONSHIP and a phone, not a name — so a row is\n * either wholly blank (ignored) or has both. `name` is synthesised from the\n * relationship in the mapper, since `emergencyContactSchema` requires one.\n */\nexport const emergencyContactRowSchema = z\n .object({\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.relationship === '') {\n ctx.addIssue({ code: 'custom', path: ['relationship'], message: 'Add the relationship' });\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 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({ code: 'custom', path: ['dob'], 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: ['dob'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dob'], message: 'Check the year of birth' });\n } else if (age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dob'], 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\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 = { relationship: '', phone: '' };\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 const emergencyContacts = v.contacts\n .filter((c) => c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n // The form captures a relationship, not a name; it doubles as the label.\n name: c.relationship.trim(),\n phone: asPhone(c.phone),\n relationship: c.relationship.trim(),\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"]}
package/dist/area.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Area lookup API — the contract behind the location picker
3
+ * =========================================================
4
+ * `/api/v1/areas/*` reads reform-owned master data (countries, states,
5
+ * districts, and the eight sub-district types) so a GymMonk address can be
6
+ * PICKED rather than typed.
7
+ *
8
+ * These shapes live here, not in either app, for the reason the whole package
9
+ * exists: the backend and the web client were each declaring their own
10
+ * `AreaOption` and `LocalityOption`, which is two definitions of one wire
11
+ * format and therefore a drift waiting to happen — the backend adding a field
12
+ * or renaming one could not fail the client's build. Declared once, both sides
13
+ * import it and a change breaks loudly at compile time.
14
+ *
15
+ * Everything here is READ-ONLY. Master data is owned by reform-backend;
16
+ * GymMonk never writes to it.
17
+ *
18
+ * @module area
19
+ */
20
+ import { z } from 'zod';
21
+ /** One pickable area: the master_db id, and the name to show for it. */
22
+ export declare const areaOptionSchema: z.ZodObject<{
23
+ id: z.ZodString;
24
+ name: z.ZodString;
25
+ }, z.core.$strip>;
26
+ export type AreaOption = z.infer<typeof areaOptionSchema>;
27
+ /**
28
+ * A locality result also carries its area TYPE.
29
+ *
30
+ * That is not decoration. One district holds rows of eight different types, and
31
+ * names repeat across them — reform's master data has "Nakraunda" as both a
32
+ * Village and a Hamlet under Dehradun. Merged into one alphabetical list, the
33
+ * type is the only thing that tells the two apart.
34
+ */
35
+ export declare const localityOptionSchema: z.ZodObject<{
36
+ id: z.ZodString;
37
+ name: z.ZodString;
38
+ type: z.ZodEnum<{
39
+ SUB_DISTRICT: "SUB_DISTRICT";
40
+ BLOCK: "BLOCK";
41
+ GRAM_PANCHAYAT: "GRAM_PANCHAYAT";
42
+ VILLAGE: "VILLAGE";
43
+ HAMLET: "HAMLET";
44
+ URBAN_BODY: "URBAN_BODY";
45
+ URBAN_WARD: "URBAN_WARD";
46
+ LOCALITY: "LOCALITY";
47
+ }>;
48
+ }, z.core.$strip>;
49
+ export type LocalityOption = z.infer<typeof localityOptionSchema>;
50
+ /**
51
+ * One page of localities.
52
+ *
53
+ * `hasMore` rather than a total count: a large district holds thousands of
54
+ * rows, and an infinite-scrolling list only ever needs to know whether to keep
55
+ * going. The server derives it by fetching one row beyond the page, which
56
+ * avoids a second `countDocuments` over the same filter.
57
+ */
58
+ export declare const localityPageSchema: z.ZodObject<{
59
+ items: z.ZodArray<z.ZodObject<{
60
+ id: z.ZodString;
61
+ name: z.ZodString;
62
+ type: z.ZodEnum<{
63
+ SUB_DISTRICT: "SUB_DISTRICT";
64
+ BLOCK: "BLOCK";
65
+ GRAM_PANCHAYAT: "GRAM_PANCHAYAT";
66
+ VILLAGE: "VILLAGE";
67
+ HAMLET: "HAMLET";
68
+ URBAN_BODY: "URBAN_BODY";
69
+ URBAN_WARD: "URBAN_WARD";
70
+ LOCALITY: "LOCALITY";
71
+ }>;
72
+ }, z.core.$strip>>;
73
+ hasMore: z.ZodBoolean;
74
+ }, z.core.$strip>;
75
+ export type LocalityPage = z.infer<typeof localityPageSchema>;
76
+ /**
77
+ * Ancestor ids and coordinates for one area, resolved once when an address is
78
+ * saved and then denormalized onto it — so reading an address never touches
79
+ * master data again.
80
+ *
81
+ * `coords` is nullable, not optional: "we looked and this chain has no
82
+ * coordinates" is a real answer, and collapsing it to a missing field would
83
+ * make it indistinguishable from a response that failed to include it.
84
+ */
85
+ export declare const areaLineageSchema: z.ZodObject<{
86
+ ancestry: z.ZodArray<z.ZodString>;
87
+ coords: z.ZodNullable<z.ZodObject<{
88
+ lat: z.ZodNumber;
89
+ lng: z.ZodNumber;
90
+ source: z.ZodString;
91
+ }, z.core.$strip>>;
92
+ }, z.core.$strip>;
93
+ export type AreaLineage = z.infer<typeof areaLineageSchema>;
94
+ /** GET /areas/districts */
95
+ export declare const districtsQuerySchema: z.ZodObject<{
96
+ stateId: z.ZodString;
97
+ q: z.ZodOptional<z.ZodString>;
98
+ }, z.core.$strip>;
99
+ export type DistrictsQuery = z.infer<typeof districtsQuerySchema>;
100
+ /** Rows per locality page. Mirrored by the client's infinite-scroll hook. */
101
+ export declare const LOCALITY_PAGE_SIZE = 50;
102
+ /** Ceiling on a caller-supplied `limit`, so one request cannot pull a district. */
103
+ export declare const LOCALITY_MAX_LIMIT = 100;
104
+ /**
105
+ * GET /areas/localities
106
+ *
107
+ * `page`/`limit` are coerced because they arrive as query strings, and clamped
108
+ * here rather than in the handler so the frontend sees the same bounds it will
109
+ * be held to.
110
+ */
111
+ export declare const localitiesQuerySchema: z.ZodObject<{
112
+ districtId: z.ZodString;
113
+ q: z.ZodOptional<z.ZodString>;
114
+ page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
115
+ limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
116
+ }, z.core.$strip>;
117
+ export type LocalitiesQuery = z.infer<typeof localitiesQuerySchema>;
118
+ /** GET /areas/lineage/:areaId */
119
+ export declare const areaLineageParamsSchema: z.ZodObject<{
120
+ areaId: z.ZodString;
121
+ }, z.core.$strip>;
122
+ export type AreaLineageParams = z.infer<typeof areaLineageParamsSchema>;
123
+ //# sourceMappingURL=area.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"area.d.ts","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,wEAAwE;AACxE,eAAO,MAAM,gBAAgB;;;iBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAE/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;iBAG7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAO5D,2BAA2B;AAC3B,eAAO,MAAM,oBAAoB;;;iBAG/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,iCAAiC;AACjC,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}