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.
- package/dist/add-person-form.d.ts +79 -90
- package/dist/add-person-form.d.ts.map +1 -1
- package/dist/add-person-form.js +19 -56
- package/dist/add-person-form.js.map +1 -1
- package/dist/area.d.ts +123 -0
- package/dist/area.d.ts.map +1 -0
- package/dist/area.js +95 -0
- package/dist/area.js.map +1 -0
- package/dist/center.d.ts +102 -120
- package/dist/center.d.ts.map +1 -1
- package/dist/check-in.js +1 -1
- package/dist/check-in.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/member-profile.d.ts +102 -120
- package/dist/member-profile.d.ts.map +1 -1
- package/dist/member.d.ts +68 -80
- package/dist/member.d.ts.map +1 -1
- package/dist/messages.d.ts +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +1 -1
- package/dist/messages.js.map +1 -1
- package/dist/onboarding-form.d.ts +373 -27
- package/dist/onboarding-form.d.ts.map +1 -1
- package/dist/onboarding-form.js +19 -22
- package/dist/onboarding-form.js.map +1 -1
- package/dist/onboarding.d.ts +136 -160
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/organisation.d.ts +102 -120
- package/dist/organisation.d.ts.map +1 -1
- package/dist/shared.d.ts +126 -41
- package/dist/shared.d.ts.map +1 -1
- package/dist/shared.js +108 -5
- package/dist/shared.js.map +1 -1
- package/dist/staff.d.ts +68 -80
- package/dist/staff.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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;
|
|
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"}
|
package/dist/add-person-form.js
CHANGED
|
@@ -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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
23
|
+
import { ageInYears, isRealDate, PHONE_REGEX } from './common.js';
|
|
24
24
|
import { emergencyContactSchema } from './member-profile.js';
|
|
25
|
-
import { addressSchema, bloodGroupSchema, genderSchema
|
|
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
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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"}
|