gymmonk-schema 0.23.0 → 0.25.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 +21 -3
- package/dist/add-person-form.d.ts.map +1 -1
- package/dist/add-person-form.js +28 -11
- package/dist/add-person-form.js.map +1 -1
- package/dist/announcement.d.ts +119 -2
- package/dist/announcement.d.ts.map +1 -1
- package/dist/announcement.js +108 -3
- package/dist/announcement.js.map +1 -1
- package/dist/center.d.ts +16 -2
- package/dist/center.d.ts.map +1 -1
- package/dist/center.js +19 -0
- package/dist/center.js.map +1 -1
- package/dist/gym-registration.d.ts +3 -0
- package/dist/gym-registration.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/location.d.ts +110 -0
- package/dist/location.d.ts.map +1 -0
- package/dist/location.js +133 -0
- package/dist/location.js.map +1 -0
- package/dist/onboarding-form.d.ts +20 -0
- package/dist/onboarding-form.d.ts.map +1 -1
- package/dist/onboarding-form.js +54 -36
- package/dist/onboarding-form.js.map +1 -1
- package/dist/onboarding.d.ts +3 -0
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/organisation.d.ts +24 -4
- package/dist/organisation.d.ts.map +1 -1
- package/dist/organisation.js +9 -0
- package/dist/organisation.js.map +1 -1
- package/dist/profile-edit-form.d.ts +138 -0
- package/dist/profile-edit-form.d.ts.map +1 -0
- package/dist/profile-edit-form.js +83 -0
- package/dist/profile-edit-form.js.map +1 -0
- package/dist/session.d.ts +3 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +8 -2
- package/dist/session.js.map +1 -1
- package/package.json +1 -1
|
@@ -35,15 +35,30 @@ export declare const ADD_PERSON_MAX_AGE_YEARS = 100;
|
|
|
35
35
|
/** Matches `createMemberBodySchema.emergencyContacts`. */
|
|
36
36
|
export declare const MAX_EMERGENCY_CONTACTS = 5;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
* either wholly blank (ignored) or
|
|
40
|
-
*
|
|
38
|
+
* One emergency-contact row: WHO to call, on what number, and how they are
|
|
39
|
+
* related. A row is either wholly blank (ignored) or names a person.
|
|
40
|
+
*
|
|
41
|
+
* ── Why `name` exists now ───────────────────────────────────────────────────
|
|
42
|
+
* This form used to collect a relationship and a phone with no name at all,
|
|
43
|
+
* and the mapper wrote the relationship INTO `name` because
|
|
44
|
+
* `emergencyContactSchema` requires one. The member's own surfaces store a real
|
|
45
|
+
* name, so the two halves of the product disagreed about what the field held,
|
|
46
|
+
* and the disagreement destroyed data: a member onboards with "Priya", the
|
|
47
|
+
* owner opens Edit Member and sees Relationship prefilled "Priya" (the reader
|
|
48
|
+
* fell back to `name`), corrects it to "Sister", saves — and the contact is now
|
|
49
|
+
* called Sister. Whoever has to ring that number in an emergency is left with
|
|
50
|
+
* a word instead of a person.
|
|
51
|
+
*
|
|
52
|
+
* `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is
|
|
53
|
+
* useful context, not something to block a desk entry over.
|
|
41
54
|
*/
|
|
42
55
|
export declare const emergencyContactRowSchema: z.ZodObject<{
|
|
56
|
+
name: z.ZodString;
|
|
43
57
|
relationship: z.ZodString;
|
|
44
58
|
phone: z.ZodString;
|
|
45
59
|
}, z.core.$strip>;
|
|
46
60
|
export interface EmergencyContactRow {
|
|
61
|
+
name: string;
|
|
47
62
|
relationship: string;
|
|
48
63
|
phone: string;
|
|
49
64
|
}
|
|
@@ -108,6 +123,7 @@ export declare const addMemberFormSchema: z.ZodObject<{
|
|
|
108
123
|
idProofUrl: z.ZodString;
|
|
109
124
|
avatarUrl: z.ZodString;
|
|
110
125
|
contacts: z.ZodArray<z.ZodObject<{
|
|
126
|
+
name: z.ZodString;
|
|
111
127
|
relationship: z.ZodString;
|
|
112
128
|
phone: z.ZodString;
|
|
113
129
|
}, z.core.$strip>>;
|
|
@@ -191,6 +207,7 @@ export declare const addStaffFormSchema: z.ZodObject<{
|
|
|
191
207
|
idProofUrl: z.ZodString;
|
|
192
208
|
avatarUrl: z.ZodString;
|
|
193
209
|
contacts: z.ZodArray<z.ZodObject<{
|
|
210
|
+
name: z.ZodString;
|
|
194
211
|
relationship: z.ZodString;
|
|
195
212
|
phone: z.ZodString;
|
|
196
213
|
}, z.core.$strip>>;
|
|
@@ -280,6 +297,7 @@ export declare const editMemberFormSchema: z.ZodObject<{
|
|
|
280
297
|
idProofUrl: z.ZodString;
|
|
281
298
|
avatarUrl: z.ZodString;
|
|
282
299
|
contacts: z.ZodArray<z.ZodObject<{
|
|
300
|
+
name: z.ZodString;
|
|
283
301
|
relationship: z.ZodString;
|
|
284
302
|
phone: z.ZodString;
|
|
285
303
|
}, z.core.$strip>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,KAAK,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,IAAI,CAAC;AASxC
|
|
1
|
+
{"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,KAAK,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,IAAI,CAAC;AASxC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,yBAAyB;;;;iBAuBlC,CAAC;AAEL,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA2ED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY5B,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,GAAG,IAAI,CAAC;IAC5C,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,GAAG,EAAE,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAIF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO3B,CAAC;AAEL,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,GAAG;IACxE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,GAAG,IAAI,CAAC;CAC9C,CAAC;AAgBF,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,EAAE,mBAIrC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,mBAInC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,kBAIlC,CAAC;AAwDF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,WAAW,CAAC,EAAE,oBAAoB,GACjC,gBAAgB,CAMlB;AAED,iDAAiD;AACjD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,eAAe,CAM7E;AAID;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY7B,CAAC;AAEL,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG;IACvD,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,GAAG,gBAAgB,CAYjF"}
|
package/dist/add-person-form.js
CHANGED
|
@@ -38,24 +38,38 @@ export const MAX_EMERGENCY_CONTACTS = 5;
|
|
|
38
38
|
const requiredChoice = (inner, message) => inner.nullable().refine((v) => v !== null, { message });
|
|
39
39
|
// ─── One emergency-contact row ───────────────────────────────────────────────
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
* either wholly blank (ignored) or
|
|
43
|
-
*
|
|
41
|
+
* One emergency-contact row: WHO to call, on what number, and how they are
|
|
42
|
+
* related. A row is either wholly blank (ignored) or names a person.
|
|
43
|
+
*
|
|
44
|
+
* ── Why `name` exists now ───────────────────────────────────────────────────
|
|
45
|
+
* This form used to collect a relationship and a phone with no name at all,
|
|
46
|
+
* and the mapper wrote the relationship INTO `name` because
|
|
47
|
+
* `emergencyContactSchema` requires one. The member's own surfaces store a real
|
|
48
|
+
* name, so the two halves of the product disagreed about what the field held,
|
|
49
|
+
* and the disagreement destroyed data: a member onboards with "Priya", the
|
|
50
|
+
* owner opens Edit Member and sees Relationship prefilled "Priya" (the reader
|
|
51
|
+
* fell back to `name`), corrects it to "Sister", saves — and the contact is now
|
|
52
|
+
* called Sister. Whoever has to ring that number in an emergency is left with
|
|
53
|
+
* a word instead of a person.
|
|
54
|
+
*
|
|
55
|
+
* `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is
|
|
56
|
+
* useful context, not something to block a desk entry over.
|
|
44
57
|
*/
|
|
45
58
|
export const emergencyContactRowSchema = z
|
|
46
59
|
.object({
|
|
60
|
+
name: z.string().trim().max(120),
|
|
47
61
|
relationship: z.string().trim().max(60),
|
|
48
62
|
phone: z.string().trim(),
|
|
49
63
|
})
|
|
50
64
|
.superRefine((v, ctx) => {
|
|
51
|
-
const blank = v.relationship === '' && v.phone === '';
|
|
65
|
+
const blank = v.name === '' && v.relationship === '' && v.phone === '';
|
|
52
66
|
if (blank)
|
|
53
67
|
return;
|
|
54
|
-
if (v.
|
|
68
|
+
if (v.name === '') {
|
|
55
69
|
ctx.addIssue({
|
|
56
70
|
code: 'custom',
|
|
57
|
-
path: ['
|
|
58
|
-
message:
|
|
71
|
+
path: ['name'],
|
|
72
|
+
message: "Add the contact's name",
|
|
59
73
|
});
|
|
60
74
|
}
|
|
61
75
|
if (!PHONE_REGEX.test(v.phone)) {
|
|
@@ -168,6 +182,7 @@ const EMPTY_BASE = {
|
|
|
168
182
|
};
|
|
169
183
|
/** One blank row so the section renders with a contact to fill. */
|
|
170
184
|
export const EMPTY_EMERGENCY_CONTACT = {
|
|
185
|
+
name: '',
|
|
171
186
|
relationship: '',
|
|
172
187
|
phone: '',
|
|
173
188
|
};
|
|
@@ -201,13 +216,15 @@ function sharedBody(v) {
|
|
|
201
216
|
// Already a complete `Address` or null — the drawer cannot produce anything
|
|
202
217
|
// in between, so there is nothing to assemble or re-check here.
|
|
203
218
|
const address = v.address ?? undefined;
|
|
219
|
+
// A row with nothing in it is not a contact. Anything else has already been
|
|
220
|
+
// through `emergencyContactRowSchema`, which requires a name and a valid
|
|
221
|
+
// phone the moment a row is touched.
|
|
204
222
|
const emergencyContacts = v.contacts
|
|
205
|
-
.filter((c) => c.relationship !== '' || c.phone !== '')
|
|
223
|
+
.filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')
|
|
206
224
|
.map((c) => emergencyContactSchema.parse({
|
|
207
|
-
|
|
208
|
-
name: c.relationship.trim(),
|
|
225
|
+
name: c.name.trim(),
|
|
209
226
|
phone: asPhone(c.phone),
|
|
210
|
-
relationship: c.relationship
|
|
227
|
+
relationship: orUndefined(c.relationship),
|
|
211
228
|
}))
|
|
212
229
|
.slice(0, MAX_EMERGENCY_CONTACTS);
|
|
213
230
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema } 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({\n code: 'custom',\n path: ['relationship'],\n message: 'Add the relationship',\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n email: z.email('Enter a valid email'),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /** `YYYY-MM-DD` from a native date input; blank when not recorded. */\n dob: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /**\n * The whole address, or null. Picked through the location drawer, which\n * hands back a complete `Address` — area chain, ids, names, ancestry,\n * coordinates — or nothing. It is therefore atomic: the \"half-filled\n * address\" this schema used to guard against can no longer be expressed.\n */\n address: addressSchema.nullable(),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n /** GCS URLs — both upload on pick, so these are never files. */\n idProofUrl: z.string(),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(v: { dob: string }, ctx: z.RefinementCtx): void {\n if (v.dob === '') return;\n\n const parts = v.dob.split('-');\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n\n if (parts.length !== 3 || !isRealDate(year, month, day)) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'That date does not exist',\n });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n } else if (age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddMemberFormValues = {\n name: string;\n email: string;\n phone: string;\n dob: string;\n gender: z.infer<typeof genderSchema> | null;\n address: Address | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n idProofUrl: string;\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n trainerId: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n email: '',\n phone: '',\n dob: '',\n gender: null,\n address: null,\n bloodGroup: '' as const,\n idProofUrl: '',\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\n // Already a complete `Address` or null — the drawer cannot produce anything\n // in between, so there is nothing to assemble or re-check here.\n const address = v.address ?? undefined;\n\n 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\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member reuses the add form's every rule, plus the Aadhaar last-four\n * the desk may not have taken at signup.\n *\n * It is a separate schema rather than the same one because the two forms differ\n * in what \"blank\" means. On the add form a blank optional is simply not\n * provided; on the edit form it is an instruction to CLEAR a value that is\n * already stored, and the mapper has to send `null` rather than omit the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /** Blank when not recorded; exactly four digits otherwise. */\n aadhaarLast4: z\n .string()\n .trim()\n .refine((v) => v === '' || /^\\d{4}$/.test(v), 'Enter the last four digits'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n aadhaarLast4: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The four identity fields go\n * along too; the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n address: values.address,\n idProofUrl: values.idProofUrl.trim(),\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n aadhaarLast4: values.aadhaarLast4.trim() === '' ? null : values.aadhaarLast4.trim(),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema } 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 * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n email: z.email('Enter a valid email'),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /** `YYYY-MM-DD` from a native date input; blank when not recorded. */\n dob: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /**\n * The whole address, or null. Picked through the location drawer, which\n * hands back a complete `Address` — area chain, ids, names, ancestry,\n * coordinates — or nothing. It is therefore atomic: the \"half-filled\n * address\" this schema used to guard against can no longer be expressed.\n */\n address: addressSchema.nullable(),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n /** GCS URLs — both upload on pick, so these are never files. */\n idProofUrl: z.string(),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(v: { dob: string }, ctx: z.RefinementCtx): void {\n if (v.dob === '') return;\n\n const parts = v.dob.split('-');\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n\n if (parts.length !== 3 || !isRealDate(year, month, day)) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'That date does not exist',\n });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n } else if (age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddMemberFormValues = {\n name: string;\n email: string;\n phone: string;\n dob: string;\n gender: z.infer<typeof genderSchema> | null;\n address: Address | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n idProofUrl: string;\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n trainerId: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n email: '',\n phone: '',\n dob: '',\n gender: null,\n address: null,\n bloodGroup: '' as const,\n idProofUrl: '',\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\n // Already a complete `Address` or null — the drawer cannot produce anything\n // in between, so there is nothing to assemble or re-check here.\n const address = v.address ?? undefined;\n\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n email: v.email.trim(),\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n dob: orUndefined(v.dob),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n address,\n emergencyContacts,\n idProofUrl: orUndefined(v.idProofUrl),\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n };\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member reuses the add form's every rule, plus the Aadhaar last-four\n * the desk may not have taken at signup.\n *\n * It is a separate schema rather than the same one because the two forms differ\n * in what \"blank\" means. On the add form a blank optional is simply not\n * provided; on the edit form it is an instruction to CLEAR a value that is\n * already stored, and the mapper has to send `null` rather than omit the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /** Blank when not recorded; exactly four digits otherwise. */\n aadhaarLast4: z\n .string()\n .trim()\n .refine((v) => v === '' || /^\\d{4}$/.test(v), 'Enter the last four digits'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n aadhaarLast4: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The four identity fields go\n * along too; the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n address: values.address,\n idProofUrl: values.idProofUrl.trim(),\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n aadhaarLast4: values.aadhaarLast4.trim() === '' ? null : values.aadhaarLast4.trim(),\n };\n}\n"]}
|
package/dist/announcement.d.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gymmonk-schema — Owner announcements
|
|
3
3
|
* ====================================
|
|
4
|
-
* Broadcasts an owner sends to a center's members (now or scheduled).
|
|
5
|
-
* as notifications + push.
|
|
4
|
+
* Broadcasts an owner sends to a center's members and staff (now or scheduled).
|
|
5
|
+
* Delivered as notifications + push.
|
|
6
|
+
*
|
|
7
|
+
* SINGLE SOURCE OF TRUTH. The listing query, the update body and the
|
|
8
|
+
* past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two
|
|
9
|
+
* hand-kept mirrors before this. They are here so a field can only be added
|
|
10
|
+
* once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,
|
|
11
|
+
* it silently drops the field somewhere between the browser and the database.
|
|
6
12
|
*
|
|
7
13
|
* @module gymmonk-schema/announcement
|
|
8
14
|
*/
|
|
@@ -34,10 +40,25 @@ export declare const announcementSchema: z.ZodObject<{
|
|
|
34
40
|
}>;
|
|
35
41
|
scheduledAt: z.ZodNullable<z.ZodISODateTime>;
|
|
36
42
|
sentAt: z.ZodNullable<z.ZodISODateTime>;
|
|
43
|
+
deliveredCount: z.ZodNullable<z.ZodNumber>;
|
|
44
|
+
deliveryFailedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
37
45
|
createdAt: z.ZodISODateTime;
|
|
38
46
|
updatedAt: z.ZodISODateTime;
|
|
39
47
|
}, z.core.$strip>;
|
|
40
48
|
export type Announcement = z.infer<typeof announcementSchema>;
|
|
49
|
+
/**
|
|
50
|
+
* How far in the past a `scheduledAt` may sit and still be treated as "now".
|
|
51
|
+
*
|
|
52
|
+
* Not zero. The browser builds the instant from two wall-clock inputs and the
|
|
53
|
+
* request then crosses a network, so an owner who picks the next minute and
|
|
54
|
+
* taps promptly can arrive a few seconds "late" through no fault of their own.
|
|
55
|
+
* Refusing that would be a validation error for doing exactly the right thing.
|
|
56
|
+
* Anything older is a genuine mistake: it would be delivered by the very next
|
|
57
|
+
* sweep while the screen called it Scheduled.
|
|
58
|
+
*/
|
|
59
|
+
export declare const SCHEDULE_GRACE_MS: number;
|
|
60
|
+
export declare const PAST_SCHEDULE_MESSAGE = "Pick a time in the future";
|
|
61
|
+
export declare function isScheduleInPast(iso: string, now?: Date): boolean;
|
|
41
62
|
export declare const createAnnouncementBodySchema: z.ZodObject<{
|
|
42
63
|
icon: z.ZodDefault<z.ZodEnum<{
|
|
43
64
|
megaphone: "megaphone";
|
|
@@ -53,4 +74,100 @@ export declare const createAnnouncementBodySchema: z.ZodObject<{
|
|
|
53
74
|
}>>;
|
|
54
75
|
}, z.core.$strip>;
|
|
55
76
|
export type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;
|
|
77
|
+
/** The create body plus the center it belongs to, as the route receives it. */
|
|
78
|
+
export declare const createAnnouncementWithCenterSchema: z.ZodIntersection<z.ZodObject<{
|
|
79
|
+
icon: z.ZodDefault<z.ZodEnum<{
|
|
80
|
+
megaphone: "megaphone";
|
|
81
|
+
warning: "warning";
|
|
82
|
+
}>>;
|
|
83
|
+
title: z.ZodString;
|
|
84
|
+
body: z.ZodString;
|
|
85
|
+
scheduledAt: z.ZodOptional<z.ZodISODateTime>;
|
|
86
|
+
status: z.ZodDefault<z.ZodEnum<{
|
|
87
|
+
draft: "draft";
|
|
88
|
+
scheduled: "scheduled";
|
|
89
|
+
sent: "sent";
|
|
90
|
+
}>>;
|
|
91
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
92
|
+
centerId: z.ZodString;
|
|
93
|
+
}, z.core.$strip>>;
|
|
94
|
+
export type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;
|
|
95
|
+
/**
|
|
96
|
+
* Edit a draft, reschedule a queued one, or send either.
|
|
97
|
+
*
|
|
98
|
+
* Every field is optional because this is a PATCH: an owner fixing a typo sends
|
|
99
|
+
* the title alone. `status` is how a draft finally goes out, which is the whole
|
|
100
|
+
* reason this exists — before it there was no update endpoint at all, so "Save
|
|
101
|
+
* as Draft" wrote a row that could only ever be deleted.
|
|
102
|
+
*
|
|
103
|
+
* `scheduledAt` accepts null explicitly, meaning "drop the schedule", which is
|
|
104
|
+
* different from omitting it ("leave the schedule alone"). Conflating the two
|
|
105
|
+
* would make it impossible to turn a scheduled announcement back into a draft.
|
|
106
|
+
*/
|
|
107
|
+
export declare const updateAnnouncementBodySchema: z.ZodObject<{
|
|
108
|
+
icon: z.ZodOptional<z.ZodEnum<{
|
|
109
|
+
megaphone: "megaphone";
|
|
110
|
+
warning: "warning";
|
|
111
|
+
}>>;
|
|
112
|
+
title: z.ZodOptional<z.ZodString>;
|
|
113
|
+
body: z.ZodOptional<z.ZodString>;
|
|
114
|
+
status: z.ZodOptional<z.ZodEnum<{
|
|
115
|
+
draft: "draft";
|
|
116
|
+
scheduled: "scheduled";
|
|
117
|
+
sent: "sent";
|
|
118
|
+
}>>;
|
|
119
|
+
scheduledAt: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
|
|
120
|
+
}, z.core.$strip>;
|
|
121
|
+
export type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;
|
|
122
|
+
/**
|
|
123
|
+
* How the list is ordered.
|
|
124
|
+
*
|
|
125
|
+
* `newest` is the default because the screen's job is "what did we just send".
|
|
126
|
+
* `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows
|
|
127
|
+
* that HAVE one: in a mixed list every draft and every already-sent row sorts as
|
|
128
|
+
* null and groups at the front, so the UI offers it under the Scheduled filter
|
|
129
|
+
* alone.
|
|
130
|
+
*/
|
|
131
|
+
export declare const announcementSortSchema: z.ZodEnum<{
|
|
132
|
+
newest: "newest";
|
|
133
|
+
oldest: "oldest";
|
|
134
|
+
upcoming: "upcoming";
|
|
135
|
+
}>;
|
|
136
|
+
export type AnnouncementSort = z.infer<typeof announcementSortSchema>;
|
|
137
|
+
/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */
|
|
138
|
+
export declare const announcementStatusFilterSchema: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodEnum<{
|
|
139
|
+
draft: "draft";
|
|
140
|
+
scheduled: "scheduled";
|
|
141
|
+
sent: "sent";
|
|
142
|
+
}>]>;
|
|
143
|
+
export type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;
|
|
144
|
+
/**
|
|
145
|
+
* The listing query.
|
|
146
|
+
*
|
|
147
|
+
* Everything arrives as a STRING off the query string, so `paginationSchema`
|
|
148
|
+
* coerces page and limit. Without the coercion `page=2` fails validation and
|
|
149
|
+
* the client gets a 400 for a link it built correctly.
|
|
150
|
+
*/
|
|
151
|
+
export declare const listAnnouncementsQuerySchema: z.ZodObject<{
|
|
152
|
+
page: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
153
|
+
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
154
|
+
centerId: z.ZodString;
|
|
155
|
+
q: z.ZodOptional<z.ZodString>;
|
|
156
|
+
status: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodEnum<{
|
|
157
|
+
draft: "draft";
|
|
158
|
+
scheduled: "scheduled";
|
|
159
|
+
sent: "sent";
|
|
160
|
+
}>]>>;
|
|
161
|
+
sort: z.ZodDefault<z.ZodEnum<{
|
|
162
|
+
newest: "newest";
|
|
163
|
+
oldest: "oldest";
|
|
164
|
+
upcoming: "upcoming";
|
|
165
|
+
}>>;
|
|
166
|
+
}, z.core.$strip>;
|
|
167
|
+
export type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;
|
|
168
|
+
/** Just the center, for routes that identify the row by id but still scope it. */
|
|
169
|
+
export declare const centerScopeQuerySchema: z.ZodObject<{
|
|
170
|
+
centerId: z.ZodString;
|
|
171
|
+
}, z.core.$strip>;
|
|
172
|
+
export type CenterScopeQuery = z.infer<typeof centerScopeQuerySchema>;
|
|
56
173
|
//# sourceMappingURL=announcement.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,wBAAwB;;;;EAAyC,CAAC;AAC/E,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAI1E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;iBA4B7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,QAAgB,CAAC;AAE/C,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AAEjE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAI7E;AAID,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;iBAwBrC,CAAC;AACL,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,+EAA+E;AAC/E,eAAO,MAAM,kCAAkC;;;;;;;;;;;;;;;kBAE9C,CAAC;AACF,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAIlG;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;iBAMvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAIlF;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;;;EAA2C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,iFAAiF;AACjF,eAAO,MAAM,8BAA8B;;;;IAAwD,CAAC;AACpG,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;iBAMvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,kFAAkF;AAClF,eAAO,MAAM,sBAAsB;;iBAAyC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
package/dist/announcement.js
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* gymmonk-schema — Owner announcements
|
|
3
3
|
* ====================================
|
|
4
|
-
* Broadcasts an owner sends to a center's members (now or scheduled).
|
|
5
|
-
* as notifications + push.
|
|
4
|
+
* Broadcasts an owner sends to a center's members and staff (now or scheduled).
|
|
5
|
+
* Delivered as notifications + push.
|
|
6
|
+
*
|
|
7
|
+
* SINGLE SOURCE OF TRUTH. The listing query, the update body and the
|
|
8
|
+
* past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two
|
|
9
|
+
* hand-kept mirrors before this. They are here so a field can only be added
|
|
10
|
+
* once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,
|
|
11
|
+
* it silently drops the field somewhere between the browser and the database.
|
|
6
12
|
*
|
|
7
13
|
* @module gymmonk-schema/announcement
|
|
8
14
|
*/
|
|
9
15
|
import { z } from 'zod';
|
|
10
|
-
import { isoDateTimeSchema, objectIdSchema } from './common.js';
|
|
16
|
+
import { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';
|
|
11
17
|
export const announcementIconSchema = z.enum(['megaphone', 'warning']);
|
|
12
18
|
export const announcementStatusSchema = z.enum(['draft', 'scheduled', 'sent']);
|
|
13
19
|
// ─── Entity ──────────────────────────────────────────────────────────────────
|
|
@@ -20,9 +26,45 @@ export const announcementSchema = z.object({
|
|
|
20
26
|
status: announcementStatusSchema,
|
|
21
27
|
scheduledAt: isoDateTimeSchema.nullable(),
|
|
22
28
|
sentAt: isoDateTimeSchema.nullable(),
|
|
29
|
+
/**
|
|
30
|
+
* How many people it actually reached, once delivery has been attempted.
|
|
31
|
+
*
|
|
32
|
+
* `null` means "not attempted yet", which is NOT the same as `0` ("attempted,
|
|
33
|
+
* nobody to tell"). An owner broadcasting to an empty roster and an owner
|
|
34
|
+
* whose broadcast has not run yet are different situations, and collapsing
|
|
35
|
+
* both to zero would leave the screen unable to say which.
|
|
36
|
+
*/
|
|
37
|
+
deliveredCount: z.number().int().nullable(),
|
|
38
|
+
/**
|
|
39
|
+
* Set when fan-out threw, cleared when a later attempt succeeds.
|
|
40
|
+
*
|
|
41
|
+
* A row is marked `sent` BEFORE fan-out (it prevents a duplicate broadcast to
|
|
42
|
+
* an entire gym), so without this a failure left no trace but a log line while
|
|
43
|
+
* the owner's screen said Sent.
|
|
44
|
+
*/
|
|
45
|
+
deliveryFailedAt: isoDateTimeSchema.nullable(),
|
|
23
46
|
createdAt: isoDateTimeSchema,
|
|
24
47
|
updatedAt: isoDateTimeSchema,
|
|
25
48
|
});
|
|
49
|
+
// ─── Scheduling guard ────────────────────────────────────────────────────────
|
|
50
|
+
/**
|
|
51
|
+
* How far in the past a `scheduledAt` may sit and still be treated as "now".
|
|
52
|
+
*
|
|
53
|
+
* Not zero. The browser builds the instant from two wall-clock inputs and the
|
|
54
|
+
* request then crosses a network, so an owner who picks the next minute and
|
|
55
|
+
* taps promptly can arrive a few seconds "late" through no fault of their own.
|
|
56
|
+
* Refusing that would be a validation error for doing exactly the right thing.
|
|
57
|
+
* Anything older is a genuine mistake: it would be delivered by the very next
|
|
58
|
+
* sweep while the screen called it Scheduled.
|
|
59
|
+
*/
|
|
60
|
+
export const SCHEDULE_GRACE_MS = 2 * 60 * 1000;
|
|
61
|
+
export const PAST_SCHEDULE_MESSAGE = 'Pick a time in the future';
|
|
62
|
+
export function isScheduleInPast(iso, now = new Date()) {
|
|
63
|
+
const at = Date.parse(iso);
|
|
64
|
+
if (Number.isNaN(at))
|
|
65
|
+
return false; // shape is the format check's problem
|
|
66
|
+
return at < now.getTime() - SCHEDULE_GRACE_MS;
|
|
67
|
+
}
|
|
26
68
|
// ─── Create body ─────────────────────────────────────────────────────────────
|
|
27
69
|
export const createAnnouncementBodySchema = z
|
|
28
70
|
.object({
|
|
@@ -36,5 +78,68 @@ export const createAnnouncementBodySchema = z
|
|
|
36
78
|
.refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {
|
|
37
79
|
message: 'Pick a schedule time',
|
|
38
80
|
path: ['scheduledAt'],
|
|
81
|
+
})
|
|
82
|
+
/**
|
|
83
|
+
* A scheduled time must be in the FUTURE. Without this, yesterday 9am
|
|
84
|
+
* validated cleanly, was stored as `scheduled`, and went out on the next sweep
|
|
85
|
+
* seconds later: the owner saw "Scheduled" and the members got it at once.
|
|
86
|
+
*
|
|
87
|
+
* Drafts are exempt on purpose. Nothing is going out, so a stale time on one
|
|
88
|
+
* is not yet a mistake.
|
|
89
|
+
*/
|
|
90
|
+
.refine((v) => v.status !== 'scheduled' || !v.scheduledAt || !isScheduleInPast(v.scheduledAt), {
|
|
91
|
+
message: PAST_SCHEDULE_MESSAGE,
|
|
92
|
+
path: ['scheduledAt'],
|
|
93
|
+
});
|
|
94
|
+
/** The create body plus the center it belongs to, as the route receives it. */
|
|
95
|
+
export const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.and(z.object({ centerId: objectIdSchema }));
|
|
96
|
+
// ─── Update body ─────────────────────────────────────────────────────────────
|
|
97
|
+
/**
|
|
98
|
+
* Edit a draft, reschedule a queued one, or send either.
|
|
99
|
+
*
|
|
100
|
+
* Every field is optional because this is a PATCH: an owner fixing a typo sends
|
|
101
|
+
* the title alone. `status` is how a draft finally goes out, which is the whole
|
|
102
|
+
* reason this exists — before it there was no update endpoint at all, so "Save
|
|
103
|
+
* as Draft" wrote a row that could only ever be deleted.
|
|
104
|
+
*
|
|
105
|
+
* `scheduledAt` accepts null explicitly, meaning "drop the schedule", which is
|
|
106
|
+
* different from omitting it ("leave the schedule alone"). Conflating the two
|
|
107
|
+
* would make it impossible to turn a scheduled announcement back into a draft.
|
|
108
|
+
*/
|
|
109
|
+
export const updateAnnouncementBodySchema = z.object({
|
|
110
|
+
icon: announcementIconSchema.optional(),
|
|
111
|
+
title: z.string().trim().min(1, 'Title is required').max(120).optional(),
|
|
112
|
+
body: z.string().trim().min(1, 'Message is required').max(250).optional(),
|
|
113
|
+
status: announcementStatusSchema.optional(),
|
|
114
|
+
scheduledAt: isoDateTimeSchema.nullable().optional(),
|
|
115
|
+
});
|
|
116
|
+
// ─── Listing query ───────────────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* How the list is ordered.
|
|
119
|
+
*
|
|
120
|
+
* `newest` is the default because the screen's job is "what did we just send".
|
|
121
|
+
* `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows
|
|
122
|
+
* that HAVE one: in a mixed list every draft and every already-sent row sorts as
|
|
123
|
+
* null and groups at the front, so the UI offers it under the Scheduled filter
|
|
124
|
+
* alone.
|
|
125
|
+
*/
|
|
126
|
+
export const announcementSortSchema = z.enum(['newest', 'oldest', 'upcoming']);
|
|
127
|
+
/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */
|
|
128
|
+
export const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);
|
|
129
|
+
/**
|
|
130
|
+
* The listing query.
|
|
131
|
+
*
|
|
132
|
+
* Everything arrives as a STRING off the query string, so `paginationSchema`
|
|
133
|
+
* coerces page and limit. Without the coercion `page=2` fails validation and
|
|
134
|
+
* the client gets a 400 for a link it built correctly.
|
|
135
|
+
*/
|
|
136
|
+
export const listAnnouncementsQuerySchema = paginationSchema.extend({
|
|
137
|
+
centerId: objectIdSchema,
|
|
138
|
+
/** Free text over title and body. Trimmed; empty means "no search". */
|
|
139
|
+
q: z.string().trim().max(120).optional(),
|
|
140
|
+
status: announcementStatusFilterSchema.default('all'),
|
|
141
|
+
sort: announcementSortSchema.default('newest'),
|
|
39
142
|
});
|
|
143
|
+
/** Just the center, for routes that identify the row by id but still scope it. */
|
|
144
|
+
export const centerScopeQuerySchema = z.object({ centerId: objectIdSchema });
|
|
40
145
|
//# sourceMappingURL=announcement.js.map
|
package/dist/announcement.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcement.js","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC;AAGvE,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;AAG/E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,EAAE,EAAE,cAAc;IAClB,QAAQ,EAAE,cAAc;IACxB,IAAI,EAAE,sBAAsB;IAC5B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,MAAM,EAAE,wBAAwB;IAChC,WAAW,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACzC,MAAM,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACpC,SAAS,EAAE,iBAAiB;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC;KAC1C,MAAM,CAAC;IACN,IAAI,EAAE,sBAAsB,CAAC,OAAO,CAAC,WAAW,CAAC;IACjD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC7D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,qBAAqB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC;IAC9D,yEAAyE;IACzE,WAAW,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACzC,MAAM,EAAE,wBAAwB,CAAC,OAAO,CAAC,MAAM,CAAC;CACjD,CAAC;KACD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,CAAC,WAAW,IAAI,IAAI,EAAE;IAChE,OAAO,EAAE,sBAAsB;IAC/B,IAAI,EAAE,CAAC,aAAa,CAAC;CACtB,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — Owner announcements\n * ====================================\n * Broadcasts an owner sends to a center's members (now or scheduled). Delivered\n * as notifications + push.\n *\n * @module gymmonk-schema/announcement\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema } from './common.js';\n\nexport const announcementIconSchema = z.enum(['megaphone', 'warning']);\nexport type AnnouncementIcon = z.infer<typeof announcementIconSchema>;\n\nexport const announcementStatusSchema = z.enum(['draft', 'scheduled', 'sent']);\nexport type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const announcementSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n icon: announcementIconSchema,\n title: z.string(),\n body: z.string(),\n status: announcementStatusSchema,\n scheduledAt: isoDateTimeSchema.nullable(),\n sentAt: isoDateTimeSchema.nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Announcement = z.infer<typeof announcementSchema>;\n\n// ─── Create body ─────────────────────────────────────────────────────────────\n\nexport const createAnnouncementBodySchema = z\n .object({\n icon: announcementIconSchema.default('megaphone'),\n title: z.string().trim().min(1, 'Title is required').max(120),\n body: z.string().trim().min(1, 'Message is required').max(250),\n /** Required when `status` is `scheduled`; otherwise sent immediately. */\n scheduledAt: isoDateTimeSchema.optional(),\n status: announcementStatusSchema.default('sent'),\n })\n .refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {\n message: 'Pick a schedule time',\n path: ['scheduledAt'],\n });\nexport type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"announcement.js","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Owner announcements\n * ====================================\n * Broadcasts an owner sends to a center's members and staff (now or scheduled).\n * Delivered as notifications + push.\n *\n * SINGLE SOURCE OF TRUTH. The listing query, the update body and the\n * past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two\n * hand-kept mirrors before this. They are here so a field can only be added\n * once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,\n * it silently drops the field somewhere between the browser and the database.\n *\n * @module gymmonk-schema/announcement\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';\n\nexport const announcementIconSchema = z.enum(['megaphone', 'warning']);\nexport type AnnouncementIcon = z.infer<typeof announcementIconSchema>;\n\nexport const announcementStatusSchema = z.enum(['draft', 'scheduled', 'sent']);\nexport type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const announcementSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n icon: announcementIconSchema,\n title: z.string(),\n body: z.string(),\n status: announcementStatusSchema,\n scheduledAt: isoDateTimeSchema.nullable(),\n sentAt: isoDateTimeSchema.nullable(),\n /**\n * How many people it actually reached, once delivery has been attempted.\n *\n * `null` means \"not attempted yet\", which is NOT the same as `0` (\"attempted,\n * nobody to tell\"). An owner broadcasting to an empty roster and an owner\n * whose broadcast has not run yet are different situations, and collapsing\n * both to zero would leave the screen unable to say which.\n */\n deliveredCount: z.number().int().nullable(),\n /**\n * Set when fan-out threw, cleared when a later attempt succeeds.\n *\n * A row is marked `sent` BEFORE fan-out (it prevents a duplicate broadcast to\n * an entire gym), so without this a failure left no trace but a log line while\n * the owner's screen said Sent.\n */\n deliveryFailedAt: isoDateTimeSchema.nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Announcement = z.infer<typeof announcementSchema>;\n\n// ─── Scheduling guard ────────────────────────────────────────────────────────\n\n/**\n * How far in the past a `scheduledAt` may sit and still be treated as \"now\".\n *\n * Not zero. The browser builds the instant from two wall-clock inputs and the\n * request then crosses a network, so an owner who picks the next minute and\n * taps promptly can arrive a few seconds \"late\" through no fault of their own.\n * Refusing that would be a validation error for doing exactly the right thing.\n * Anything older is a genuine mistake: it would be delivered by the very next\n * sweep while the screen called it Scheduled.\n */\nexport const SCHEDULE_GRACE_MS = 2 * 60 * 1000;\n\nexport const PAST_SCHEDULE_MESSAGE = 'Pick a time in the future';\n\nexport function isScheduleInPast(iso: string, now: Date = new Date()): boolean {\n const at = Date.parse(iso);\n if (Number.isNaN(at)) return false; // shape is the format check's problem\n return at < now.getTime() - SCHEDULE_GRACE_MS;\n}\n\n// ─── Create body ─────────────────────────────────────────────────────────────\n\nexport const createAnnouncementBodySchema = z\n .object({\n icon: announcementIconSchema.default('megaphone'),\n title: z.string().trim().min(1, 'Title is required').max(120),\n body: z.string().trim().min(1, 'Message is required').max(250),\n /** Required when `status` is `scheduled`; otherwise sent immediately. */\n scheduledAt: isoDateTimeSchema.optional(),\n status: announcementStatusSchema.default('sent'),\n })\n .refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {\n message: 'Pick a schedule time',\n path: ['scheduledAt'],\n })\n /**\n * A scheduled time must be in the FUTURE. Without this, yesterday 9am\n * validated cleanly, was stored as `scheduled`, and went out on the next sweep\n * seconds later: the owner saw \"Scheduled\" and the members got it at once.\n *\n * Drafts are exempt on purpose. Nothing is going out, so a stale time on one\n * is not yet a mistake.\n */\n .refine((v) => v.status !== 'scheduled' || !v.scheduledAt || !isScheduleInPast(v.scheduledAt), {\n message: PAST_SCHEDULE_MESSAGE,\n path: ['scheduledAt'],\n });\nexport type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;\n\n/** The create body plus the center it belongs to, as the route receives it. */\nexport const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.and(\n z.object({ centerId: objectIdSchema }),\n);\nexport type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;\n\n// ─── Update body ─────────────────────────────────────────────────────────────\n\n/**\n * Edit a draft, reschedule a queued one, or send either.\n *\n * Every field is optional because this is a PATCH: an owner fixing a typo sends\n * the title alone. `status` is how a draft finally goes out, which is the whole\n * reason this exists — before it there was no update endpoint at all, so \"Save\n * as Draft\" wrote a row that could only ever be deleted.\n *\n * `scheduledAt` accepts null explicitly, meaning \"drop the schedule\", which is\n * different from omitting it (\"leave the schedule alone\"). Conflating the two\n * would make it impossible to turn a scheduled announcement back into a draft.\n */\nexport const updateAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.optional(),\n title: z.string().trim().min(1, 'Title is required').max(120).optional(),\n body: z.string().trim().min(1, 'Message is required').max(250).optional(),\n status: announcementStatusSchema.optional(),\n scheduledAt: isoDateTimeSchema.nullable().optional(),\n});\nexport type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;\n\n// ─── Listing query ───────────────────────────────────────────────────────────\n\n/**\n * How the list is ordered.\n *\n * `newest` is the default because the screen's job is \"what did we just send\".\n * `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows\n * that HAVE one: in a mixed list every draft and every already-sent row sorts as\n * null and groups at the front, so the UI offers it under the Scheduled filter\n * alone.\n */\nexport const announcementSortSchema = z.enum(['newest', 'oldest', 'upcoming']);\nexport type AnnouncementSort = z.infer<typeof announcementSortSchema>;\n\n/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */\nexport const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);\nexport type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;\n\n/**\n * The listing query.\n *\n * Everything arrives as a STRING off the query string, so `paginationSchema`\n * coerces page and limit. Without the coercion `page=2` fails validation and\n * the client gets a 400 for a link it built correctly.\n */\nexport const listAnnouncementsQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema,\n /** Free text over title and body. Trimmed; empty means \"no search\". */\n q: z.string().trim().max(120).optional(),\n status: announcementStatusFilterSchema.default('all'),\n sort: announcementSortSchema.default('newest'),\n});\nexport type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;\n\n/** Just the center, for routes that identify the row by id but still scope it. */\nexport const centerScopeQuerySchema = z.object({ centerId: objectIdSchema });\nexport type CenterScopeQuery = z.infer<typeof centerScopeQuerySchema>;\n"]}
|
package/dist/center.d.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type CenterStatus = z.infer<typeof centerStatusSchema>;
|
|
|
37
37
|
export declare const gymCodeSchema: z.ZodString;
|
|
38
38
|
export type GymCode = z.infer<typeof gymCodeSchema>;
|
|
39
39
|
export declare const createCenterBodySchema: z.ZodObject<{
|
|
40
|
+
checkInRadiusM: z.ZodOptional<z.ZodNumber>;
|
|
41
|
+
mapLinkUrl: z.ZodOptional<z.ZodString>;
|
|
40
42
|
name: z.ZodString;
|
|
41
43
|
description: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
|
|
42
44
|
types: z.ZodArray<z.ZodEnum<{
|
|
@@ -137,6 +139,8 @@ export declare const createCenterBodySchema: z.ZodObject<{
|
|
|
137
139
|
}, z.core.$strip>;
|
|
138
140
|
export type CreateCenterBody = z.infer<typeof createCenterBodySchema>;
|
|
139
141
|
export declare const updateCenterBodySchema: z.ZodObject<{
|
|
142
|
+
checkInRadiusM: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
143
|
+
mapLinkUrl: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
140
144
|
name: z.ZodOptional<z.ZodString>;
|
|
141
145
|
description: z.ZodOptional<z.ZodPreprocess<z.ZodOptional<z.ZodString>>>;
|
|
142
146
|
types: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
@@ -237,6 +241,18 @@ export declare const updateCenterBodySchema: z.ZodObject<{
|
|
|
237
241
|
}, z.core.$strip>;
|
|
238
242
|
export type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;
|
|
239
243
|
export declare const centerSchema: z.ZodObject<{
|
|
244
|
+
checkInRadiusM: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
245
|
+
createdAt: z.ZodISODateTime;
|
|
246
|
+
updatedAt: z.ZodISODateTime;
|
|
247
|
+
mapLink: z.ZodOptional<z.ZodObject<{
|
|
248
|
+
url: z.ZodString;
|
|
249
|
+
lat: z.ZodOptional<z.ZodNumber>;
|
|
250
|
+
lng: z.ZodOptional<z.ZodNumber>;
|
|
251
|
+
}, z.core.$strip>>;
|
|
252
|
+
location: z.ZodOptional<z.ZodObject<{
|
|
253
|
+
type: z.ZodLiteral<"Point">;
|
|
254
|
+
coordinates: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
|
|
255
|
+
}, z.core.$strip>>;
|
|
240
256
|
id: z.ZodString;
|
|
241
257
|
organisationId: z.ZodString;
|
|
242
258
|
code: z.ZodString;
|
|
@@ -342,8 +358,6 @@ export declare const centerSchema: z.ZodObject<{
|
|
|
342
358
|
inactive: "inactive";
|
|
343
359
|
}>;
|
|
344
360
|
memberCount: z.ZodNumber;
|
|
345
|
-
createdAt: z.ZodISODateTime;
|
|
346
|
-
updatedAt: z.ZodISODateTime;
|
|
347
361
|
}, z.core.$strip>;
|
|
348
362
|
export type Center = z.infer<typeof centerSchema>;
|
|
349
363
|
/**
|
package/dist/center.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"center.d.ts","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,yDAAyD;AACzD,eAAO,MAAM,YAAY,8IAaf,CAAC;AACX,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;EAAuB,CAAC;AACrD,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAI1D,eAAO,MAAM,kBAAkB;;;EAAiC,CAAC;AACjE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,aAGwB,CAAC;AACnD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAIpD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAqCjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0BvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,yEAAyE;AACzE,eAAO,MAAM,eAAe;;;;;;;;;iBAkB1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,oBAAoB;;;;;;;;iBAwB7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAKpE,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|