gymmonk-schema 0.29.0 → 0.31.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 +0 -2
- package/dist/add-person-form.d.ts.map +1 -1
- package/dist/add-person-form.js +1 -3
- package/dist/add-person-form.js.map +1 -1
- package/dist/center.d.ts +1 -0
- package/dist/center.d.ts.map +1 -1
- package/dist/center.js +19 -1
- package/dist/center.js.map +1 -1
- package/dist/location.d.ts +23 -0
- package/dist/location.d.ts.map +1 -1
- package/dist/location.js +50 -0
- package/dist/location.js.map +1 -1
- package/dist/member-profile.d.ts +15 -0
- package/dist/member-profile.d.ts.map +1 -1
- package/dist/member-profile.js +16 -1
- package/dist/member-profile.js.map +1 -1
- package/dist/member.d.ts.map +1 -1
- package/dist/member.js +2 -2
- package/dist/member.js.map +1 -1
- package/dist/profile-edit-form.d.ts +21 -44
- package/dist/profile-edit-form.d.ts.map +1 -1
- package/dist/profile-edit-form.js +24 -15
- package/dist/profile-edit-form.js.map +1 -1
- package/dist/session.d.ts +38 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +34 -0
- package/dist/session.js.map +1 -1
- package/dist/staff.js +2 -2
- package/dist/staff.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,8 +32,6 @@ import { staffRoleSchema } from './staff.js';
|
|
|
32
32
|
*/
|
|
33
33
|
export declare const ADD_PERSON_MIN_AGE_YEARS = 5;
|
|
34
34
|
export declare const ADD_PERSON_MAX_AGE_YEARS = 100;
|
|
35
|
-
/** Matches `createMemberBodySchema.emergencyContacts`. */
|
|
36
|
-
export declare const MAX_EMERGENCY_CONTACTS = 5;
|
|
37
35
|
/**
|
|
38
36
|
* One emergency-contact row: WHO to call, on what number, and how they are
|
|
39
37
|
* related. A row is either wholly blank (ignored) or names a person.
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"add-person-form.d.ts","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,EAAE,KAAK,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAI7C;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAC1C,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAS5C;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,yBAAyB;;;;iBAuBlC,CAAC;AAEL,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AA2ED,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY5B,CAAC;AAEL,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,GAAG,IAAI,CAAC;IAC5C,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,GAAG,EAAE,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAIF,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO3B,CAAC;AAEL,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,mBAAmB,EAAE,WAAW,CAAC,GAAG;IACxE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,GAAG,IAAI,CAAC;CAC9C,CAAC;AAgBF,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,EAAE,mBAIrC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,mBAInC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,kBAIlC,CAAC;AAwDF;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,mBAAmB,EAC3B,WAAW,CAAC,EAAE,oBAAoB,GACjC,gBAAgB,CAMlB;AAED,iDAAiD;AACjD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,eAAe,CAM7E;AAID;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY7B,CAAC;AAEL,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,GAAG;IACvD,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,oBAAoB,GAAG,gBAAgB,CAYjF"}
|
package/dist/add-person-form.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { z } from 'zod';
|
|
23
23
|
import { ageInYears, isRealDate, PHONE_REGEX } from './common.js';
|
|
24
|
-
import { emergencyContactSchema } from './member-profile.js';
|
|
24
|
+
import { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';
|
|
25
25
|
import { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';
|
|
26
26
|
import { staffRoleSchema } from './staff.js';
|
|
27
27
|
// ─── Bounds ──────────────────────────────────────────────────────────────────
|
|
@@ -32,8 +32,6 @@ import { staffRoleSchema } from './staff.js';
|
|
|
32
32
|
*/
|
|
33
33
|
export const ADD_PERSON_MIN_AGE_YEARS = 5;
|
|
34
34
|
export const ADD_PERSON_MAX_AGE_YEARS = 100;
|
|
35
|
-
/** Matches `createMemberBodySchema.emergencyContacts`. */
|
|
36
|
-
export const MAX_EMERGENCY_CONTACTS = 5;
|
|
37
35
|
// ─── Field-level rules ───────────────────────────────────────────────────────
|
|
38
36
|
const requiredChoice = (inner, message) => inner.nullable().refine((v) => v !== null, { message });
|
|
39
37
|
// ─── One emergency-contact row ───────────────────────────────────────────────
|
|
@@ -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 * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n email: z.email('Enter a valid email'),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /** `YYYY-MM-DD` from a native date input; blank when not recorded. */\n dob: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /**\n * The whole address, or null. Picked through the location drawer, which\n * hands back a complete `Address` — area chain, ids, names, ancestry,\n * coordinates — or nothing. It is therefore atomic: the \"half-filled\n * address\" this schema used to guard against can no longer be expressed.\n */\n address: addressSchema.nullable(),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n /** GCS URLs — both upload on pick, so these are never files. */\n idProofUrl: z.string(),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(v: { dob: string }, ctx: z.RefinementCtx): void {\n if (v.dob === '') return;\n\n const parts = v.dob.split('-');\n const year = Number(parts[0]);\n const month = Number(parts[1]);\n const day = Number(parts[2]);\n\n if (parts.length !== 3 || !isRealDate(year, month, day)) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'That date does not exist',\n });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n } else if (age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({\n code: 'custom',\n path: ['dob'],\n message: 'Check the year of birth',\n });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddMemberFormValues = {\n name: string;\n email: string;\n phone: string;\n dob: string;\n gender: z.infer<typeof genderSchema> | null;\n address: Address | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n idProofUrl: string;\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n trainerId: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\nexport const addStaffFormSchema = z\n .object({\n ...addPersonBaseShape,\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\n role: z.infer<typeof staffRoleSchema> | null;\n};\n\n// ─── Empty states ────────────────────────────────────────────────────────────\n\nconst EMPTY_BASE = {\n name: '',\n email: '',\n phone: '',\n dob: '',\n gender: null,\n address: null,\n bloodGroup: '' as const,\n idProofUrl: '',\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\n // Already a complete `Address` or null — the drawer cannot produce anything\n // in between, so there is nothing to assemble or re-check here.\n const address = v.address ?? undefined;\n\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n email: v.email.trim(),\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n dob: orUndefined(v.dob),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n address,\n emergencyContacts,\n idProofUrl: orUndefined(v.idProofUrl),\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n };\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member reuses the add form's every rule, plus the Aadhaar last-four\n * the desk may not have taken at signup.\n *\n * It is a separate schema rather than the same one because the two forms differ\n * in what \"blank\" means. On the add form a blank optional is simply not\n * provided; on the edit form it is an instruction to CLEAR a value that is\n * already stored, and the mapper has to send `null` rather than omit the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /** Blank when not recorded; exactly four digits otherwise. */\n aadhaarLast4: z\n .string()\n .trim()\n .refine((v) => v === '' || /^\\d{4}$/.test(v), 'Enter the last four digits'),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n aadhaarLast4: string;\n};\n\n/**\n * Validated edit form → `PATCH /members/:id` body.\n *\n * Sends every field, including the cleared ones as `null`, because the owner\n * emptying a field is an edit like any other. The four identity fields go\n * along too; the server drops them once the member has claimed their account.\n */\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\n const shared = sharedBody(values);\n return {\n ...shared,\n dob: shared.dob ?? null,\n bloodGroup: shared.bloodGroup ?? null,\n address: values.address,\n idProofUrl: values.idProofUrl.trim(),\n avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n aadhaarLast4: values.aadhaarLast4.trim() === '' ? null : values.aadhaarLast4.trim(),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\n * ========================================================\n * The client-side validation contract for the owner's desk-entry forms, and the\n * mappers that turn a filled form into the `createMember` / `createStaff` API\n * bodies.\n *\n * Why these live here rather than in the client, and why they are distinct from\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\n * states that the API body must never see. Expressing the same rules over the\n * form shape here keeps\n * every message, bound and regex defined once — the client cannot drift from\n * what the server will accept, and the coercion from one shape to the other is\n * part of the contract instead of ad-hoc code in a component.\n *\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\n *\n * @module gymmonk-schema/add-person-form\n */\n\nimport { z } from 'zod';\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { type Address, addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\nimport type { CreateStaffBody } from './staff.js';\nimport { staffRoleSchema } from './staff.js';\n\n// ─── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\n * because a gym also enrols juniors with a guardian present, and the date is\n * typed by staff rather than by the person themselves.\n */\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\n\n// ─── 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/center.d.ts
CHANGED
|
@@ -242,6 +242,7 @@ export declare const updateCenterBodySchema: z.ZodObject<{
|
|
|
242
242
|
export type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;
|
|
243
243
|
export declare const centerSchema: z.ZodObject<{
|
|
244
244
|
checkInRadiusM: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
245
|
+
timezone: z.ZodDefault<z.ZodString>;
|
|
245
246
|
createdAt: z.ZodISODateTime;
|
|
246
247
|
updatedAt: z.ZodISODateTime;
|
|
247
248
|
mapLink: z.ZodOptional<z.ZodObject<{
|
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;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
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA4CvB,CAAC;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,yEAAyE;AACzE,eAAO,MAAM,eAAe;;;;;;;;;iBAkB1B,CAAC;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,eAAO,MAAM,oBAAoB;;;;;;;;iBAwB7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAKpE,eAAO,MAAM,qBAAqB;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
package/dist/center.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { isoDateTimeSchema, objectIdSchema, optionalEmailSchema, optionalPhoneSchema, optionalTrimmedString, phoneSchema, } from './common.js';
|
|
12
|
-
import { mapLinkInputSchema, placePinSchema } from './location.js';
|
|
12
|
+
import { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';
|
|
13
13
|
import { addressSchema, weeklyHoursSchema } from './shared.js';
|
|
14
14
|
// ─── Center type ─────────────────────────────────────────────────────────────
|
|
15
15
|
/** The activity types a center offers (multi-select). */
|
|
@@ -103,6 +103,24 @@ export const centerSchema = z.object({
|
|
|
103
103
|
// attendance is unenforceable rather than failing for a gym without one.
|
|
104
104
|
...placePinSchema.shape,
|
|
105
105
|
checkInRadiusM: z.number().int().nullable().optional(),
|
|
106
|
+
/**
|
|
107
|
+
* The centre's own timezone, as an IANA identifier ("Asia/Kolkata").
|
|
108
|
+
*
|
|
109
|
+
* WHY THE CENTRE'S AND NOT THE VIEWER'S. Attendance is filed under a calendar
|
|
110
|
+
* DAY, and a day is not timezone-free — whichever zone derives it defines when
|
|
111
|
+
* it starts. Deriving it from whoever is looking would put one visit on
|
|
112
|
+
* different days for the member and the owner, and would make an owner's daily
|
|
113
|
+
* roll-up change depending on where they opened the app. A visit belongs to the
|
|
114
|
+
* gym's operating day; only the display of times follows the reader.
|
|
115
|
+
*
|
|
116
|
+
* An IANA NAME, never a UTC offset. Offsets are a property of an instant, not
|
|
117
|
+
* of a place: they move with DST, so a stored "+05:30" is already wrong for
|
|
118
|
+
* half the year anywhere that observes it.
|
|
119
|
+
*
|
|
120
|
+
* Server-derived from the centre's pin, never accepted from a client — the
|
|
121
|
+
* same rule the coordinates follow, and for the same reason.
|
|
122
|
+
*/
|
|
123
|
+
timezone: timezoneSchema,
|
|
106
124
|
createdAt: isoDateTimeSchema,
|
|
107
125
|
updatedAt: isoDateTimeSchema,
|
|
108
126
|
});
|
package/dist/center.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { mapLinkInputSchema, placePinSchema } from './location.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/** The activity types a center offers (multi-select). */\nexport const CENTER_TYPES = [\n 'gym',\n 'crossfit',\n 'yoga',\n 'pilates',\n 'zumba',\n 'cardio',\n 'strength',\n 'mma',\n 'boxing',\n 'swimming',\n 'aerobics',\n 'personal-training',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n/**\n * The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.\n * Server-generated and unique; never accepted in a create body.\n */\nexport const gymCodeSchema = z\n .string()\n .trim()\n .regex(/^GYM-\\d{4}-\\d{3,}$/, 'Invalid gym code');\nexport type GymCode = z.infer<typeof gymCodeSchema>;\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one type'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n /**\n * A pasted Google Maps link. The server reads the coordinates out of it; the\n * client never sends them.\n *\n * This is the pin attendance is measured against, and it is NOT the same as\n * `address.coords`: that is an area centroid resolved from master data, which\n * can be a whole district's midpoint and is useless for proximity.\n */\n ...mapLinkInputSchema.shape,\n /**\n * How close a member must be for a check-in to count, metres. Optional — the\n * server applies its own default when a gym has not chosen one.\n */\n checkInRadiusM: z.number().int().min(50).max(5000).optional(),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `GYM-YYYY-NNN`. */\n code: gymCodeSchema,\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n // The resolved pin. Absent until an owner pastes a Maps link, which is why\n // attendance is unenforceable rather than failing for a gym without one.\n ...placePinSchema.shape,\n checkInRadiusM: z.number().int().nullable().optional(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: gymCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine((v) => Boolean(v.code || v.q || v.stateId || v.districtId), {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n });\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n\n// ─── Retained: nearby-gyms query ─────────────────────────────────────────────\n// Kept for the gym-search route the owner console's repo still serves. See the\n// note in `onboarding.ts`.\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"center.js","sourceRoot":"","sources":["../src/center.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Center (branch)\n * ================================\n * A physical gym location belonging to an organisation. Carries the public gym\n * code members use to join, opening hours, equipment scope, and contact info.\n * v1 memberships are single-center, so a Center is the unit a member joins.\n *\n * @module gymmonk-schema/center\n */\n\nimport { z } from 'zod';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalEmailSchema,\n optionalPhoneSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { mapLinkInputSchema, placePinSchema, timezoneSchema } from './location.js';\nimport { addressSchema, weeklyHoursSchema } from './shared.js';\n\n// ─── Center type ─────────────────────────────────────────────────────────────\n\n/** The activity types a center offers (multi-select). */\nexport const CENTER_TYPES = [\n 'gym',\n 'crossfit',\n 'yoga',\n 'pilates',\n 'zumba',\n 'cardio',\n 'strength',\n 'mma',\n 'boxing',\n 'swimming',\n 'aerobics',\n 'personal-training',\n] as const;\nexport const centerTypeSchema = z.enum(CENTER_TYPES);\nexport type CenterType = z.infer<typeof centerTypeSchema>;\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const centerStatusSchema = z.enum(['active', 'inactive']);\nexport type CenterStatus = z.infer<typeof centerStatusSchema>;\n\n/**\n * The public gym code a member enters at onboarding to join, `GYM-YYYY-NNN`.\n * Server-generated and unique; never accepted in a create body.\n */\nexport const gymCodeSchema = z\n .string()\n .trim()\n .regex(/^GYM-\\d{4}-\\d{3,}$/, 'Invalid gym code');\nexport type GymCode = z.infer<typeof gymCodeSchema>;\n\n// ─── Create / update bodies ──────────────────────────────────────────────────\n\nexport const createCenterBodySchema = z.object({\n name: z.string().trim().min(1, 'Center name is required').max(160),\n description: optionalTrimmedString(2000),\n types: z.array(centerTypeSchema).min(1, 'Pick at least one type'),\n address: addressSchema,\n /** Usable floor area in square feet. */\n floorAreaSqft: z.number().int().min(0).max(1_000_000).optional(),\n /**\n * How many people the branch holds at once.\n *\n * The denominator behind \"how full are we\" — the centre detail screen shows\n * it beside the active-member count, and crowd-load has no meaning without\n * it. Optional: a gym that has never measured it should not be blocked from\n * saving the rest of its details.\n */\n capacity: z.number().int().min(1).max(100_000).optional(),\n primaryPhone: phoneSchema,\n secondaryPhone: optionalPhoneSchema,\n email: optionalEmailSchema,\n website: optionalTrimmedString(200),\n hours: weeklyHoursSchema,\n /** Uploaded gallery image URLs. */\n photos: z.array(z.string().trim().max(2000)).max(20).default([]),\n /**\n * A pasted Google Maps link. The server reads the coordinates out of it; the\n * client never sends them.\n *\n * This is the pin attendance is measured against, and it is NOT the same as\n * `address.coords`: that is an area centroid resolved from master data, which\n * can be a whole district's midpoint and is useless for proximity.\n */\n ...mapLinkInputSchema.shape,\n /**\n * How close a member must be for a check-in to count, metres. Optional — the\n * server applies its own default when a gym has not chosen one.\n */\n checkInRadiusM: z.number().int().min(50).max(5000).optional(),\n});\nexport type CreateCenterBody = z.infer<typeof createCenterBodySchema>;\n\nexport const updateCenterBodySchema = createCenterBodySchema.partial();\nexport type UpdateCenterBody = z.infer<typeof updateCenterBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\nexport const centerSchema = z.object({\n id: objectIdSchema,\n organisationId: objectIdSchema,\n /** Public join code, `GYM-YYYY-NNN`. */\n code: gymCodeSchema,\n name: z.string(),\n description: z.string().nullable(),\n types: z.array(centerTypeSchema),\n address: addressSchema,\n floorAreaSqft: z.number().int().nullable(),\n capacity: z.number().int().nullable(),\n primaryPhone: z.string(),\n secondaryPhone: z.string().nullable(),\n email: z.string().nullable(),\n website: z.string().nullable(),\n hours: weeklyHoursSchema,\n photos: z.array(z.string()),\n status: centerStatusSchema,\n /** Denormalised active-member count for roster headers. */\n memberCount: z.number().int().nonnegative(),\n // The resolved pin. Absent until an owner pastes a Maps link, which is why\n // attendance is unenforceable rather than failing for a gym without one.\n ...placePinSchema.shape,\n checkInRadiusM: z.number().int().nullable().optional(),\n /**\n * The centre's own timezone, as an IANA identifier (\"Asia/Kolkata\").\n *\n * WHY THE CENTRE'S AND NOT THE VIEWER'S. Attendance is filed under a calendar\n * DAY, and a day is not timezone-free — whichever zone derives it defines when\n * it starts. Deriving it from whoever is looking would put one visit on\n * different days for the member and the owner, and would make an owner's daily\n * roll-up change depending on where they opened the app. A visit belongs to the\n * gym's operating day; only the display of times follows the reader.\n *\n * An IANA NAME, never a UTC offset. Offsets are a property of an instant, not\n * of a place: they move with DST, so a stored \"+05:30\" is already wrong for\n * half the year anywhere that observes it.\n *\n * Server-derived from the centre's pin, never accepted from a client — the\n * same rule the coordinates follow, and for the same reason.\n */\n timezone: timezoneSchema,\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Center = z.infer<typeof centerSchema>;\n\n// ─── Center stats (owner detail header) ──────────────────────────────────────\n\n/**\n * The three-up header on a centre's detail screen.\n *\n * Computed, never stored — `activeMembers` counts live memberships and\n * `monthlyRevenueInr` sums this month's successful payments, so the card can\n * never drift from the ledger the way a denormalised total would. `capacity`\n * is the one stored value, and is null until the owner fills it in.\n */\nexport const centerStatsSchema = z.object({\n capacity: z.number().int().nullable(),\n activeMembers: z.number().int().nonnegative(),\n monthlyRevenueInr: z.number().nonnegative(),\n});\nexport type CenterStats = z.infer<typeof centerStatsSchema>;\n\n// ─── Gym directory (member \"find a gym\") ─────────────────────────────────────\n\n/** A lightweight public directory result for the \"find a gym\" screen. */\nexport const nearbyGymSchema = z.object({\n id: objectIdSchema,\n code: gymCodeSchema,\n name: z.string(),\n /** The narrowest place name we hold (locality → district → state). */\n area: z.string(),\n district: z.string().nullable(),\n state: z.string().nullable(),\n /**\n * Kilometres from the coordinates the caller supplied.\n *\n * NULL, not 0, when it cannot be computed — the caller sent no position, or\n * the gym's address carries no coordinates. Zero is a real distance (\"you are\n * standing in it\") and using it as the unknown value put every un-geocoded\n * gym at the top of a nearest-first list.\n */\n distanceKm: z.number().nonnegative().nullable(),\n memberCount: z.number().int().nonnegative().optional(),\n});\nexport type NearbyGym = z.infer<typeof nearbyGymSchema>;\n\n/**\n * The gym directory has THREE ways in, and they differ in how far they reach.\n *\n * `code` exact public gym id — matches anywhere in the country, because\n * a member holding the id already knows which gym they mean and\n * may well be joining one in another state.\n * `q` name search — deliberately narrowed to ONE STATE. Gym names\n * repeat heavily (\"Fitness First\"), so a nationwide name search\n * returns a list nobody can pick from.\n * `districtId` browse — the gyms around them, nearest first when they share\n * their position.\n *\n * `stateId` / `districtId` are master-area ids (`area.ts`), matched against the\n * address `ancestry` array every center stores.\n */\nexport const GYM_SEARCH_MAX_RESULTS = 20;\n\nexport const gymSearchQuerySchema = z\n .object({\n /** Exact public gym id, e.g. `GYM-2025-001`. Unscoped by geography. */\n code: optionalTrimmedString(40),\n /** Name fragment. Requires `stateId`. */\n q: optionalTrimmedString(120),\n stateId: optionalTrimmedString(40),\n districtId: optionalTrimmedString(40),\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n limit: z.coerce\n .number()\n .int()\n .min(1)\n .max(GYM_SEARCH_MAX_RESULTS)\n .default(GYM_SEARCH_MAX_RESULTS),\n })\n .refine((v) => !v.q || Boolean(v.stateId), {\n message: 'Pick a state to search gyms by name',\n path: ['stateId'],\n })\n .refine((v) => Boolean(v.code || v.q || v.stateId || v.districtId), {\n message: 'Enter a gym id, or pick where to look',\n path: ['stateId'],\n });\nexport type GymSearchQuery = z.infer<typeof gymSearchQuerySchema>;\n\n/**\n * The directory response.\n *\n * `truncated` says the district holds more gyms than were returned, which is\n * what the screen turns into \"showing the 20 nearest — enter a gym id if yours\n * is not here\" rather than letting the member assume their gym is absent.\n */\nexport const gymSearchResultSchema = z.object({\n items: z.array(nearbyGymSchema),\n total: z.number().int().nonnegative(),\n truncated: z.boolean(),\n});\nexport type GymSearchResult = z.infer<typeof gymSearchResultSchema>;\n\n// ─── Retained: nearby-gyms query ─────────────────────────────────────────────\n// Kept for the gym-search route the owner console's repo still serves. See the\n// note in `onboarding.ts`.\nexport const nearbyGymsQuerySchema = z.object({\n lat: z.coerce.number().min(-90).max(90).optional(),\n lng: z.coerce.number().min(-180).max(180).optional(),\n q: optionalTrimmedString(120),\n});\nexport type NearbyGymsQuery = z.infer<typeof nearbyGymsQuerySchema>;\n"]}
|
package/dist/location.d.ts
CHANGED
|
@@ -86,6 +86,29 @@ export declare const placePinSchema: z.ZodObject<{
|
|
|
86
86
|
}, z.core.$strip>>;
|
|
87
87
|
}, z.core.$strip>;
|
|
88
88
|
export type PlacePin = z.infer<typeof placePinSchema>;
|
|
89
|
+
/** Every gym is in India until one is not. */
|
|
90
|
+
export declare const DEFAULT_TIMEZONE = "Asia/Kolkata";
|
|
91
|
+
/**
|
|
92
|
+
* Is this a timezone the runtime actually knows?
|
|
93
|
+
*
|
|
94
|
+
* Validated rather than trusted, because an unknown identifier does not degrade
|
|
95
|
+
* gracefully — `Intl.DateTimeFormat` THROWS on one. A bad value stored on a
|
|
96
|
+
* centre would take down every attendance write for that gym, at the moment a
|
|
97
|
+
* member scans, which is the worst possible time to discover a typo.
|
|
98
|
+
*
|
|
99
|
+
* Shared so the server validates before storing and the client can validate
|
|
100
|
+
* before offering a choice.
|
|
101
|
+
*/
|
|
102
|
+
export declare function isValidTimeZone(value: string): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* An IANA timezone identifier — `Asia/Kolkata`, `America/New_York`.
|
|
105
|
+
*
|
|
106
|
+
* A NAME, never a UTC offset. An offset is a property of an instant rather than
|
|
107
|
+
* of a place: it moves with DST, so a stored `+05:30` is already wrong for half
|
|
108
|
+
* the year anywhere that observes it, and cannot answer "when does this gym's
|
|
109
|
+
* day start" in a month's time.
|
|
110
|
+
*/
|
|
111
|
+
export declare const timezoneSchema: z.ZodDefault<z.ZodString>;
|
|
89
112
|
/**
|
|
90
113
|
* The position a check-in request carries: where the scanning device thought it
|
|
91
114
|
* was, and how sure it was.
|
package/dist/location.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"location.d.ts","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWpD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,aAMzB,CAAC;AAEL,uEAAuE;AACvE,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;iBAIxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;iBAGzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;iBAGzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAItD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB;;;;iBAUhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"location.d.ts","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAWpD;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gBAAgB,aAMzB,CAAC;AAEL,uEAAuE;AACvE,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;iBAIxB,CAAC;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;iBAGzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;iBAGzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAItD,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,iBAAiB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAmBtD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,2BAIC,CAAC;AAI7B;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB;;;;iBAUhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
|
package/dist/location.js
CHANGED
|
@@ -103,6 +103,56 @@ export const placePinSchema = z.object({
|
|
|
103
103
|
mapLink: mapLinkSchema.optional(),
|
|
104
104
|
location: geoPointSchema.optional(),
|
|
105
105
|
});
|
|
106
|
+
// ─── Timezone ────────────────────────────────────────────────────────────────
|
|
107
|
+
/** Every gym is in India until one is not. */
|
|
108
|
+
export const DEFAULT_TIMEZONE = 'Asia/Kolkata';
|
|
109
|
+
/**
|
|
110
|
+
* Is this a timezone the runtime actually knows?
|
|
111
|
+
*
|
|
112
|
+
* Validated rather than trusted, because an unknown identifier does not degrade
|
|
113
|
+
* gracefully — `Intl.DateTimeFormat` THROWS on one. A bad value stored on a
|
|
114
|
+
* centre would take down every attendance write for that gym, at the moment a
|
|
115
|
+
* member scans, which is the worst possible time to discover a typo.
|
|
116
|
+
*
|
|
117
|
+
* Shared so the server validates before storing and the client can validate
|
|
118
|
+
* before offering a choice.
|
|
119
|
+
*/
|
|
120
|
+
export function isValidTimeZone(value) {
|
|
121
|
+
if (!value)
|
|
122
|
+
return false;
|
|
123
|
+
/**
|
|
124
|
+
* Offset forms are rejected explicitly, and this line is the whole reason the
|
|
125
|
+
* check is not a bare try/catch.
|
|
126
|
+
*
|
|
127
|
+
* ECMA-402 lets `Intl.DateTimeFormat` accept an OFFSET as a time zone, so
|
|
128
|
+
* `"+05:30"` constructs perfectly happily. It is still not a timezone: an
|
|
129
|
+
* offset cannot say when the clocks change, so storing one gives a gym a day
|
|
130
|
+
* boundary that is right until DST and silently wrong afterwards — in exactly
|
|
131
|
+
* the zones a global rollout would add.
|
|
132
|
+
*/
|
|
133
|
+
if (/^[+-]\d{2}:?\d{2}$/.test(value.trim()))
|
|
134
|
+
return false;
|
|
135
|
+
try {
|
|
136
|
+
new Intl.DateTimeFormat('en-US', { timeZone: value });
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* An IANA timezone identifier — `Asia/Kolkata`, `America/New_York`.
|
|
145
|
+
*
|
|
146
|
+
* A NAME, never a UTC offset. An offset is a property of an instant rather than
|
|
147
|
+
* of a place: it moves with DST, so a stored `+05:30` is already wrong for half
|
|
148
|
+
* the year anywhere that observes it, and cannot answer "when does this gym's
|
|
149
|
+
* day start" in a month's time.
|
|
150
|
+
*/
|
|
151
|
+
export const timezoneSchema = z
|
|
152
|
+
.string()
|
|
153
|
+
.trim()
|
|
154
|
+
.refine(isValidTimeZone, { message: 'Unknown timezone' })
|
|
155
|
+
.default(DEFAULT_TIMEZONE);
|
|
106
156
|
// ─── Check-in position ───────────────────────────────────────────────────────
|
|
107
157
|
/**
|
|
108
158
|
* The position a check-in request carries: where the scanning device thought it
|
package/dist/location.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"location.js","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Where a place physically is\n * ============================================\n * The pasted Google Maps link an owner gives for their headquarters and each\n * centre, and the coordinates read out of it.\n *\n * VALIDATION LIVES HERE, RESOLUTION DOES NOT. Deciding whether a string is a\n * Google Maps link is pure and belongs in one place for both repos. FOLLOWING\n * that link — a server-side fetch of a short URL — stays in gymmonk-backend\n * (`shared/lib/resolve-map-link`), because it is I/O and because it is the\n * SSRF-shaped half that must never run in a browser.\n *\n * WHY THE CLIENT'S COORDINATES ARE NEVER TRUSTED\n * ----------------------------------------------\n * The registration form previews the pin as you paste, so the browser already\n * holds `{lat, lng}` and could simply send them. It must not, and no body schema\n * here accepts them. That pin decides whether a member's check-in counts, so\n * taking it from the client would let anyone replaying the request place a gym\n * on top of a member's sofa and have the geofence agree. The URL is evidence;\n * the coordinates are a conclusion, and the server draws it.\n *\n * @module gymmonk-schema/location\n */\n\nimport { z } from 'zod';\n\n/** Hosts whose links are treated as Google maps. */\nconst MAP_HOSTS = new Set([\n 'maps.google.com',\n 'www.google.com',\n 'google.com',\n 'maps.app.goo.gl',\n 'goo.gl',\n 'g.co',\n]);\n\n/** True for a link we are willing to treat as a Google map. */\nexport function isGoogleMapsUrl(raw: string): boolean {\n try {\n const url = new URL(raw.trim());\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false;\n const host = url.hostname.toLowerCase();\n // `google.co.in`, `google.co.uk` and the rest of the country domains all\n // serve the same /maps paths.\n return MAP_HOSTS.has(host) || /(^|\\.)google\\.[a-z.]{2,6}$/.test(host);\n } catch {\n return false;\n }\n}\n\n/**\n * The pasted link, validated only as far as \"plausibly a Google map\".\n *\n * Deliberately permissive about FORM and strict about HOST. The backend's\n * resolver performs an outbound request for short links, so the host allowlist\n * is a security control; the rest of the URL can be any of the half-dozen shapes\n * Maps emits, and rejecting one because it did not match a pattern would be\n * telling an owner their own share link is wrong.\n *\n * An empty string is accepted and means \"remove the link\", which is why this\n * cannot simply be `.url()`.\n */\nexport const mapLinkUrlSchema = z\n .string()\n .trim()\n .max(2000)\n .refine((v) => v === '' || isGoogleMapsUrl(v), {\n message: 'Paste a Google Maps link (from Share → Copy link)',\n });\n\n/** Mix into any create/update body that describes a physical place. */\nexport const mapLinkInputSchema = z.object({\n mapLinkUrl: mapLinkUrlSchema.optional(),\n});\nexport type MapLinkInput = z.infer<typeof mapLinkInputSchema>;\n\n/**\n * What the server stored for a resolved link.\n *\n * `lat`/`lng` are absent when the link carried no readable pin. The link is\n * still kept: \"Open in Maps\" works from the url alone, and an owner who pasted\n * something should not find the field empty next time they open the form. It\n * simply means proximity stays unenforceable for that gym, which is the truth.\n */\nexport const mapLinkSchema = z.object({\n url: z.string(),\n lat: z.number().optional(),\n lng: z.number().optional(),\n});\nexport type MapLink = z.infer<typeof mapLinkSchema>;\n\n/**\n * A GeoJSON point, in MongoDB's order.\n *\n * `[lng, lat]`, NOT `[lat, lng]`. Reversing them is the classic 2dsphere bug and\n * it does not throw — it silently puts every Indian gym in the Indian Ocean and\n * the geofence then refuses every check-in.\n */\nexport const geoPointSchema = z.object({\n type: z.literal('Point'),\n coordinates: z.tuple([z.number().min(-180).max(180), z.number().min(-90).max(90)]),\n});\nexport type GeoPoint = z.infer<typeof geoPointSchema>;\n\n/**\n * What a place exposes about its position on the wire.\n *\n * Both optional: no gym is required to have a pin, and one that does not simply\n * cannot have attendance verified against it.\n */\nexport const placePinSchema = z.object({\n mapLink: mapLinkSchema.optional(),\n location: geoPointSchema.optional(),\n});\nexport type PlacePin = z.infer<typeof placePinSchema>;\n\n// ─── Check-in position ───────────────────────────────────────────────────────\n\n/**\n * The position a check-in request carries: where the scanning device thought it\n * was, and how sure it was.\n *\n * WHY ON THE REQUEST, AND NOT READ FROM THE USER'S STORED FIX\n * The app records a member's location trail separately, and reading the latest\n * entry here would be wrong: that write happens BESIDE the scan, not before it,\n * so at the moment check-in reaches the server the trail may hold a fix from an\n * hour ago, or nothing. Attendance would then be verified against where the\n * member happened to be earlier, which is evidence of nothing.\n *\n * ALL THREE ARE OPTIONAL and stay that way. A browser without geolocation, a\n * refused permission, a fix that never arrived indoors — each is ordinary, and\n * the server treats a missing fix as unverifiable rather than as a failure.\n */\nexport const checkInGeoInputSchema = z.object({\n lat: z.number().min(-90).max(90).optional(),\n lng: z.number().min(-180).max(180).optional(),\n /**\n * The browser's own accuracy radius, metres. Bounded rather than trusted: it\n * widens the allowed radius, so an unbounded value would be a way to switch\n * the check off by claiming a very uncertain fix. The server caps the\n * allowance again on its own side.\n */\n accuracyM: z.number().min(0).max(100_000).optional(),\n});\nexport type CheckInGeoInput = z.infer<typeof checkInGeoInputSchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"location.js","sourceRoot":"","sources":["../src/location.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Where a place physically is\n * ============================================\n * The pasted Google Maps link an owner gives for their headquarters and each\n * centre, and the coordinates read out of it.\n *\n * VALIDATION LIVES HERE, RESOLUTION DOES NOT. Deciding whether a string is a\n * Google Maps link is pure and belongs in one place for both repos. FOLLOWING\n * that link — a server-side fetch of a short URL — stays in gymmonk-backend\n * (`shared/lib/resolve-map-link`), because it is I/O and because it is the\n * SSRF-shaped half that must never run in a browser.\n *\n * WHY THE CLIENT'S COORDINATES ARE NEVER TRUSTED\n * ----------------------------------------------\n * The registration form previews the pin as you paste, so the browser already\n * holds `{lat, lng}` and could simply send them. It must not, and no body schema\n * here accepts them. That pin decides whether a member's check-in counts, so\n * taking it from the client would let anyone replaying the request place a gym\n * on top of a member's sofa and have the geofence agree. The URL is evidence;\n * the coordinates are a conclusion, and the server draws it.\n *\n * @module gymmonk-schema/location\n */\n\nimport { z } from 'zod';\n\n/** Hosts whose links are treated as Google maps. */\nconst MAP_HOSTS = new Set([\n 'maps.google.com',\n 'www.google.com',\n 'google.com',\n 'maps.app.goo.gl',\n 'goo.gl',\n 'g.co',\n]);\n\n/** True for a link we are willing to treat as a Google map. */\nexport function isGoogleMapsUrl(raw: string): boolean {\n try {\n const url = new URL(raw.trim());\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false;\n const host = url.hostname.toLowerCase();\n // `google.co.in`, `google.co.uk` and the rest of the country domains all\n // serve the same /maps paths.\n return MAP_HOSTS.has(host) || /(^|\\.)google\\.[a-z.]{2,6}$/.test(host);\n } catch {\n return false;\n }\n}\n\n/**\n * The pasted link, validated only as far as \"plausibly a Google map\".\n *\n * Deliberately permissive about FORM and strict about HOST. The backend's\n * resolver performs an outbound request for short links, so the host allowlist\n * is a security control; the rest of the URL can be any of the half-dozen shapes\n * Maps emits, and rejecting one because it did not match a pattern would be\n * telling an owner their own share link is wrong.\n *\n * An empty string is accepted and means \"remove the link\", which is why this\n * cannot simply be `.url()`.\n */\nexport const mapLinkUrlSchema = z\n .string()\n .trim()\n .max(2000)\n .refine((v) => v === '' || isGoogleMapsUrl(v), {\n message: 'Paste a Google Maps link (from Share → Copy link)',\n });\n\n/** Mix into any create/update body that describes a physical place. */\nexport const mapLinkInputSchema = z.object({\n mapLinkUrl: mapLinkUrlSchema.optional(),\n});\nexport type MapLinkInput = z.infer<typeof mapLinkInputSchema>;\n\n/**\n * What the server stored for a resolved link.\n *\n * `lat`/`lng` are absent when the link carried no readable pin. The link is\n * still kept: \"Open in Maps\" works from the url alone, and an owner who pasted\n * something should not find the field empty next time they open the form. It\n * simply means proximity stays unenforceable for that gym, which is the truth.\n */\nexport const mapLinkSchema = z.object({\n url: z.string(),\n lat: z.number().optional(),\n lng: z.number().optional(),\n});\nexport type MapLink = z.infer<typeof mapLinkSchema>;\n\n/**\n * A GeoJSON point, in MongoDB's order.\n *\n * `[lng, lat]`, NOT `[lat, lng]`. Reversing them is the classic 2dsphere bug and\n * it does not throw — it silently puts every Indian gym in the Indian Ocean and\n * the geofence then refuses every check-in.\n */\nexport const geoPointSchema = z.object({\n type: z.literal('Point'),\n coordinates: z.tuple([z.number().min(-180).max(180), z.number().min(-90).max(90)]),\n});\nexport type GeoPoint = z.infer<typeof geoPointSchema>;\n\n/**\n * What a place exposes about its position on the wire.\n *\n * Both optional: no gym is required to have a pin, and one that does not simply\n * cannot have attendance verified against it.\n */\nexport const placePinSchema = z.object({\n mapLink: mapLinkSchema.optional(),\n location: geoPointSchema.optional(),\n});\nexport type PlacePin = z.infer<typeof placePinSchema>;\n\n// ─── Timezone ────────────────────────────────────────────────────────────────\n\n/** Every gym is in India until one is not. */\nexport const DEFAULT_TIMEZONE = 'Asia/Kolkata';\n\n/**\n * Is this a timezone the runtime actually knows?\n *\n * Validated rather than trusted, because an unknown identifier does not degrade\n * gracefully — `Intl.DateTimeFormat` THROWS on one. A bad value stored on a\n * centre would take down every attendance write for that gym, at the moment a\n * member scans, which is the worst possible time to discover a typo.\n *\n * Shared so the server validates before storing and the client can validate\n * before offering a choice.\n */\nexport function isValidTimeZone(value: string): boolean {\n if (!value) return false;\n /**\n * Offset forms are rejected explicitly, and this line is the whole reason the\n * check is not a bare try/catch.\n *\n * ECMA-402 lets `Intl.DateTimeFormat` accept an OFFSET as a time zone, so\n * `\"+05:30\"` constructs perfectly happily. It is still not a timezone: an\n * offset cannot say when the clocks change, so storing one gives a gym a day\n * boundary that is right until DST and silently wrong afterwards — in exactly\n * the zones a global rollout would add.\n */\n if (/^[+-]\\d{2}:?\\d{2}$/.test(value.trim())) return false;\n try {\n new Intl.DateTimeFormat('en-US', { timeZone: value });\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * An IANA timezone identifier — `Asia/Kolkata`, `America/New_York`.\n *\n * A NAME, never a UTC offset. An offset is a property of an instant rather than\n * of a place: it moves with DST, so a stored `+05:30` is already wrong for half\n * the year anywhere that observes it, and cannot answer \"when does this gym's\n * day start\" in a month's time.\n */\nexport const timezoneSchema = z\n .string()\n .trim()\n .refine(isValidTimeZone, { message: 'Unknown timezone' })\n .default(DEFAULT_TIMEZONE);\n\n// ─── Check-in position ───────────────────────────────────────────────────────\n\n/**\n * The position a check-in request carries: where the scanning device thought it\n * was, and how sure it was.\n *\n * WHY ON THE REQUEST, AND NOT READ FROM THE USER'S STORED FIX\n * The app records a member's location trail separately, and reading the latest\n * entry here would be wrong: that write happens BESIDE the scan, not before it,\n * so at the moment check-in reaches the server the trail may hold a fix from an\n * hour ago, or nothing. Attendance would then be verified against where the\n * member happened to be earlier, which is evidence of nothing.\n *\n * ALL THREE ARE OPTIONAL and stay that way. A browser without geolocation, a\n * refused permission, a fix that never arrived indoors — each is ordinary, and\n * the server treats a missing fix as unverifiable rather than as a failure.\n */\nexport const checkInGeoInputSchema = z.object({\n lat: z.number().min(-90).max(90).optional(),\n lng: z.number().min(-180).max(180).optional(),\n /**\n * The browser's own accuracy radius, metres. Bounded rather than trusted: it\n * widens the allowed radius, so an unbounded value would be a way to switch\n * the check off by claiming a very uncertain fix. The server caps the\n * allowance again on its own side.\n */\n accuracyM: z.number().min(0).max(100_000).optional(),\n});\nexport type CheckInGeoInput = z.infer<typeof checkInGeoInputSchema>;\n"]}
|
package/dist/member-profile.d.ts
CHANGED
|
@@ -30,6 +30,21 @@ export declare const bodyMetricsSchema: z.ZodObject<{
|
|
|
30
30
|
calfCm: z.ZodOptional<z.ZodNumber>;
|
|
31
31
|
}, z.core.$strip>;
|
|
32
32
|
export type BodyMetrics = z.infer<typeof bodyMetricsSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* How many emergency contacts a person may have. ONE.
|
|
35
|
+
*
|
|
36
|
+
* Lives here, beside `emergencyContactSchema`, because every surface that
|
|
37
|
+
* writes a contact has to agree on the cap: the API bodies below, the owner's
|
|
38
|
+
* desk-entry forms, and the member's own edit page. When the cap was a literal
|
|
39
|
+
* `5` repeated across four files and a separate constant in the form module,
|
|
40
|
+
* "how many contacts are there" had two answers depending on which file you
|
|
41
|
+
* opened — and the member's page could save one contact over an owner's second.
|
|
42
|
+
*
|
|
43
|
+
* The RESPONSE schemas deliberately do NOT apply it. A record written before
|
|
44
|
+
* the cap can legitimately hold more, and refusing to READ it would take the
|
|
45
|
+
* member's whole profile down over a field nobody is editing.
|
|
46
|
+
*/
|
|
47
|
+
export declare const MAX_EMERGENCY_CONTACTS = 1;
|
|
33
48
|
export declare const emergencyContactSchema: z.ZodObject<{
|
|
34
49
|
name: z.ZodString;
|
|
35
50
|
phone: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member-profile.d.ts","sourceRoot":"","sources":["../src/member-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAW5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D,eAAO,MAAM,sBAAsB;;;;iBAIjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkBxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,2CAA2C;AAC3C,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA0C,CAAC;AACrF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;EAAgC,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuD9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"member-profile.d.ts","sourceRoot":"","sources":["../src/member-profile.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAYxB;;;;GAIG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAW5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAI5D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC,eAAO,MAAM,sBAAsB;;;;iBAIjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAItE;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAkBxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF,2CAA2C;AAC3C,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA0C,CAAC;AACrF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;EAAgC,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuD9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC"}
|
package/dist/member-profile.js
CHANGED
|
@@ -33,6 +33,21 @@ export const bodyMetricsSchema = z.object({
|
|
|
33
33
|
calfCm: z.number().min(15).max(80).optional(),
|
|
34
34
|
});
|
|
35
35
|
// ─── Emergency contact ───────────────────────────────────────────────────────
|
|
36
|
+
/**
|
|
37
|
+
* How many emergency contacts a person may have. ONE.
|
|
38
|
+
*
|
|
39
|
+
* Lives here, beside `emergencyContactSchema`, because every surface that
|
|
40
|
+
* writes a contact has to agree on the cap: the API bodies below, the owner's
|
|
41
|
+
* desk-entry forms, and the member's own edit page. When the cap was a literal
|
|
42
|
+
* `5` repeated across four files and a separate constant in the form module,
|
|
43
|
+
* "how many contacts are there" had two answers depending on which file you
|
|
44
|
+
* opened — and the member's page could save one contact over an owner's second.
|
|
45
|
+
*
|
|
46
|
+
* The RESPONSE schemas deliberately do NOT apply it. A record written before
|
|
47
|
+
* the cap can legitimately hold more, and refusing to READ it would take the
|
|
48
|
+
* member's whole profile down over a field nobody is editing.
|
|
49
|
+
*/
|
|
50
|
+
export const MAX_EMERGENCY_CONTACTS = 1;
|
|
36
51
|
export const emergencyContactSchema = z.object({
|
|
37
52
|
name: z.string().trim().min(1).max(120),
|
|
38
53
|
phone: phoneSchema,
|
|
@@ -48,7 +63,7 @@ export const upsertMemberProfileBodySchema = z.object({
|
|
|
48
63
|
dob: isoDateSchema,
|
|
49
64
|
bloodGroup: bloodGroupSchema.optional(),
|
|
50
65
|
address: addressSchema.optional(),
|
|
51
|
-
emergencyContacts: z.array(emergencyContactSchema).max(
|
|
66
|
+
emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),
|
|
52
67
|
body: bodyMetricsSchema.default({}),
|
|
53
68
|
/** Uploaded ID-proof document URL (Aadhaar/PAN/etc.). */
|
|
54
69
|
idProofUrl: optionalTrimmedString(2000),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member-profile.js","sourceRoot":"","sources":["../src/member-profile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Member fitness profile\n * =======================================\n * Product-owned personal + fitness data for a gym member, separate from the\n * auth-mirrored `User` (which holds name/email/phone/avatar). One profile per\n * user, scoped to the single center they belong to.\n *\n * Body measurements are stored RAW (cm / kg); BMI and body-fat % are derived at\n * the edge, never persisted. Full Aadhaar is never stored — only its last four\n * digits and/or an uploaded ID-proof URL (DPDP data-minimisation).\n *\n * @module gymmonk-schema/member-profile\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Body measurements ───────────────────────────────────────────────────────\n\n/**\n * Raw body measurements in metric units. Height + weight anchor BMI; neck /\n * waist / hips feed the U.S.-Navy body-fat estimate; the rest are progress\n * tracking. All optional so a partial update touches only what changed.\n */\nexport const bodyMetricsSchema = z.object({\n heightCm: z.number().min(120).max(230).optional(),\n weightKg: z.number().min(30).max(300).optional(),\n neckCm: z.number().min(20).max(80).optional(),\n waistCm: z.number().min(40).max(200).optional(),\n hipsCm: z.number().min(50).max(200).optional(),\n forearmCm: z.number().min(10).max(80).optional(),\n armsCm: z.number().min(10).max(100).optional(),\n chestCm: z.number().min(40).max(200).optional(),\n thighCm: z.number().min(20).max(120).optional(),\n calfCm: z.number().min(15).max(80).optional(),\n});\nexport type BodyMetrics = z.infer<typeof bodyMetricsSchema>;\n\n// ─── Emergency contact ───────────────────────────────────────────────────────\n\nexport const emergencyContactSchema = z.object({\n name: z.string().trim().min(1).max(120),\n phone: phoneSchema,\n relationship: optionalTrimmedString(60),\n});\nexport type EmergencyContact = z.infer<typeof emergencyContactSchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\n/**\n * Upsert the member's profile. Used by member onboarding, owner \"add member\",\n * and the member's own edit-profile screen.\n */\nexport const upsertMemberProfileBodySchema = z.object({\n gender: genderSchema,\n dob: isoDateSchema,\n bloodGroup: bloodGroupSchema.optional(),\n address: addressSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(5).default([]),\n body: bodyMetricsSchema.default({}),\n /** Uploaded ID-proof document URL (Aadhaar/PAN/etc.). */\n idProofUrl: optionalTrimmedString(2000),\n /** Last four digits of Aadhaar for display only. */\n aadhaarLast4: z.preprocess(\n (v) => (typeof v === 'string' && v.trim() === '' ? undefined : v),\n z\n .string()\n .trim()\n .regex(/^\\d{4}$/, 'Enter the last 4 digits')\n .optional(),\n ),\n});\nexport type UpsertMemberProfileBody = z.infer<typeof upsertMemberProfileBodySchema>;\n\n/** Partial edit — every field optional. */\nexport const updateMemberProfileBodySchema = upsertMemberProfileBodySchema.partial();\nexport type UpdateMemberProfileBody = z.infer<typeof updateMemberProfileBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\n/**\n * Lifecycle of a member record. `invited` = the gym created it and the person\n * has not claimed their login yet (the roster still works fully); `active` = a\n * central-auth account is linked.\n */\nexport const memberRecordStatusSchema = z.enum(['invited', 'active']);\nexport type MemberRecordStatus = z.infer<typeof memberRecordStatusSchema>;\n\nexport const memberProfileSchema = z.object({\n id: objectIdSchema,\n /**\n * The linked central-auth user. NULL until the member claims their invite —\n * gyms add members at the desk long before (or without) an app login.\n */\n userId: objectIdSchema.nullable(),\n status: memberRecordStatusSchema,\n /** The address the invite was issued to; the claim must match it. */\n inviteEmail: z.string().nullable(),\n /** WhatsApp number the owner shares the invite link on. */\n invitePhone: z.string().nullable(),\n claimedAt: isoDateTimeSchema.nullable(),\n /**\n * The gym this member belongs to. NULL when they belong to none.\n *\n * Everybody signs up as a member with no gym attached and finds one\n * afterwards, so \"no gym yet\" is the ordinary starting state, not an error.\n * It is set when a gym ACCEPTS their join request (or when they claim an\n * invite a gym issued them), and cleared when they leave — which is what\n * makes joining somewhere else afterwards possible.\n */\n centerId: objectIdSchema.nullable(),\n /**\n * That gym's name, resolved server-side. Null exactly when `centerId` is.\n *\n * It rides along with the id because a member CANNOT look it up themselves:\n * the whole `/centers` router requires `manage Center`, so a member asking\n * for the name of their own gym gets a 403. Without this field, every member\n * surface that wants to name their gym either shows an id, shows nothing, or\n * fires a request that can only fail — and all three happened before it\n * existed.\n *\n * Denormalised on READ, not stored: it is resolved per request from the\n * Center, so renaming a gym is immediately correct everywhere rather than\n * leaving stale copies on every member record.\n */\n centerName: z.string().nullable(),\n /** Public member code within the gym, `GM-YYYY-NNNNN`. Server-generated. */\n memberCode: z.string(),\n gender: genderSchema,\n /** Null when the gym added the member without their date of birth. */\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n address: addressSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n body: bodyMetricsSchema,\n idProofUrl: z.string().nullable(),\n aadhaarLast4: z.string().nullable(),\n /** The workout plan the gym assigned to this member (owner action). */\n assignedWorkoutPlanId: objectIdSchema.nullable(),\n /** The trainer looking after this member (owner action). */\n assignedTrainerId: objectIdSchema.nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type MemberProfile = z.infer<typeof memberProfileSchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"member-profile.js","sourceRoot":"","sources":["../src/member-profile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Member fitness profile\n * =======================================\n * Product-owned personal + fitness data for a gym member, separate from the\n * auth-mirrored `User` (which holds name/email/phone/avatar). One profile per\n * user, scoped to the single center they belong to.\n *\n * Body measurements are stored RAW (cm / kg); BMI and body-fat % are derived at\n * the edge, never persisted. Full Aadhaar is never stored — only its last four\n * digits and/or an uploaded ID-proof URL (DPDP data-minimisation).\n *\n * @module gymmonk-schema/member-profile\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n phoneSchema,\n} from './common.js';\nimport { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Body measurements ───────────────────────────────────────────────────────\n\n/**\n * Raw body measurements in metric units. Height + weight anchor BMI; neck /\n * waist / hips feed the U.S.-Navy body-fat estimate; the rest are progress\n * tracking. All optional so a partial update touches only what changed.\n */\nexport const bodyMetricsSchema = z.object({\n heightCm: z.number().min(120).max(230).optional(),\n weightKg: z.number().min(30).max(300).optional(),\n neckCm: z.number().min(20).max(80).optional(),\n waistCm: z.number().min(40).max(200).optional(),\n hipsCm: z.number().min(50).max(200).optional(),\n forearmCm: z.number().min(10).max(80).optional(),\n armsCm: z.number().min(10).max(100).optional(),\n chestCm: z.number().min(40).max(200).optional(),\n thighCm: z.number().min(20).max(120).optional(),\n calfCm: z.number().min(15).max(80).optional(),\n});\nexport type BodyMetrics = z.infer<typeof bodyMetricsSchema>;\n\n// ─── Emergency contact ───────────────────────────────────────────────────────\n\n/**\n * How many emergency contacts a person may have. ONE.\n *\n * Lives here, beside `emergencyContactSchema`, because every surface that\n * writes a contact has to agree on the cap: the API bodies below, the owner's\n * desk-entry forms, and the member's own edit page. When the cap was a literal\n * `5` repeated across four files and a separate constant in the form module,\n * \"how many contacts are there\" had two answers depending on which file you\n * opened — and the member's page could save one contact over an owner's second.\n *\n * The RESPONSE schemas deliberately do NOT apply it. A record written before\n * the cap can legitimately hold more, and refusing to READ it would take the\n * member's whole profile down over a field nobody is editing.\n */\nexport const MAX_EMERGENCY_CONTACTS = 1;\n\nexport const emergencyContactSchema = z.object({\n name: z.string().trim().min(1).max(120),\n phone: phoneSchema,\n relationship: optionalTrimmedString(60),\n});\nexport type EmergencyContact = z.infer<typeof emergencyContactSchema>;\n\n// ─── Create / update body ────────────────────────────────────────────────────\n\n/**\n * Upsert the member's profile. Used by member onboarding, owner \"add member\",\n * and the member's own edit-profile screen.\n */\nexport const upsertMemberProfileBodySchema = z.object({\n gender: genderSchema,\n dob: isoDateSchema,\n bloodGroup: bloodGroupSchema.optional(),\n address: addressSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\n body: bodyMetricsSchema.default({}),\n /** Uploaded ID-proof document URL (Aadhaar/PAN/etc.). */\n idProofUrl: optionalTrimmedString(2000),\n /** Last four digits of Aadhaar for display only. */\n aadhaarLast4: z.preprocess(\n (v) => (typeof v === 'string' && v.trim() === '' ? undefined : v),\n z\n .string()\n .trim()\n .regex(/^\\d{4}$/, 'Enter the last 4 digits')\n .optional(),\n ),\n});\nexport type UpsertMemberProfileBody = z.infer<typeof upsertMemberProfileBodySchema>;\n\n/** Partial edit — every field optional. */\nexport const updateMemberProfileBodySchema = upsertMemberProfileBodySchema.partial();\nexport type UpdateMemberProfileBody = z.infer<typeof updateMemberProfileBodySchema>;\n\n// ─── Entity (API response) ───────────────────────────────────────────────────\n\n/**\n * Lifecycle of a member record. `invited` = the gym created it and the person\n * has not claimed their login yet (the roster still works fully); `active` = a\n * central-auth account is linked.\n */\nexport const memberRecordStatusSchema = z.enum(['invited', 'active']);\nexport type MemberRecordStatus = z.infer<typeof memberRecordStatusSchema>;\n\nexport const memberProfileSchema = z.object({\n id: objectIdSchema,\n /**\n * The linked central-auth user. NULL until the member claims their invite —\n * gyms add members at the desk long before (or without) an app login.\n */\n userId: objectIdSchema.nullable(),\n status: memberRecordStatusSchema,\n /** The address the invite was issued to; the claim must match it. */\n inviteEmail: z.string().nullable(),\n /** WhatsApp number the owner shares the invite link on. */\n invitePhone: z.string().nullable(),\n claimedAt: isoDateTimeSchema.nullable(),\n /**\n * The gym this member belongs to. NULL when they belong to none.\n *\n * Everybody signs up as a member with no gym attached and finds one\n * afterwards, so \"no gym yet\" is the ordinary starting state, not an error.\n * It is set when a gym ACCEPTS their join request (or when they claim an\n * invite a gym issued them), and cleared when they leave — which is what\n * makes joining somewhere else afterwards possible.\n */\n centerId: objectIdSchema.nullable(),\n /**\n * That gym's name, resolved server-side. Null exactly when `centerId` is.\n *\n * It rides along with the id because a member CANNOT look it up themselves:\n * the whole `/centers` router requires `manage Center`, so a member asking\n * for the name of their own gym gets a 403. Without this field, every member\n * surface that wants to name their gym either shows an id, shows nothing, or\n * fires a request that can only fail — and all three happened before it\n * existed.\n *\n * Denormalised on READ, not stored: it is resolved per request from the\n * Center, so renaming a gym is immediately correct everywhere rather than\n * leaving stale copies on every member record.\n */\n centerName: z.string().nullable(),\n /** Public member code within the gym, `GM-YYYY-NNNNN`. Server-generated. */\n memberCode: z.string(),\n gender: genderSchema,\n /** Null when the gym added the member without their date of birth. */\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n address: addressSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n body: bodyMetricsSchema,\n idProofUrl: z.string().nullable(),\n aadhaarLast4: z.string().nullable(),\n /** The workout plan the gym assigned to this member (owner action). */\n assignedWorkoutPlanId: objectIdSchema.nullable(),\n /** The trainer looking after this member (owner action). */\n assignedTrainerId: objectIdSchema.nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type MemberProfile = z.infer<typeof memberProfileSchema>;\n"]}
|
package/dist/member.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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"}
|
package/dist/member.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { isoDateSchema, isoDateTimeSchema, objectIdSchema, optionalTrimmedString, paginationSchema, phoneSchema, priceInrSchema, } from './common.js';
|
|
12
|
-
import { emergencyContactSchema, memberRecordStatusSchema } from './member-profile.js';
|
|
12
|
+
import { emergencyContactSchema, MAX_EMERGENCY_CONTACTS, memberRecordStatusSchema, } from './member-profile.js';
|
|
13
13
|
import { assignMembershipBodySchema, membershipSchema, membershipStatusSchema, } from './membership.js';
|
|
14
14
|
import { planTierSchema } from './membership-plan.js';
|
|
15
15
|
import { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';
|
|
@@ -160,7 +160,7 @@ export const createMemberBodySchema = z.object({
|
|
|
160
160
|
dob: isoDateSchema.optional(),
|
|
161
161
|
bloodGroup: bloodGroupSchema.optional(),
|
|
162
162
|
address: addressSchema.optional(),
|
|
163
|
-
emergencyContacts: z.array(emergencyContactSchema).max(
|
|
163
|
+
emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),
|
|
164
164
|
idProofUrl: optionalTrimmedString(2000),
|
|
165
165
|
avatarUrl: optionalTrimmedString(2000),
|
|
166
166
|
assignedTrainerId: objectIdSchema.optional(),
|
package/dist/member.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"member.js","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Member management (owner console)\n * ==================================================\n * Owner-facing roster + detail projections (User × MemberProfile × Membership)\n * and the \"add member\" provisioning payload. Adding a member provisions a real\n * central-auth account, so the result carries the login email + temp password.\n *\n * @module gymmonk-schema/member\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n phoneSchema,\n priceInrSchema,\n} from './common.js';\nimport { emergencyContactSchema, memberRecordStatusSchema } from './member-profile.js';\nimport {\n assignMembershipBodySchema,\n membershipSchema,\n membershipStatusSchema,\n} from './membership.js';\nimport { planTierSchema } from './membership-plan.js';\nimport { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Segments ────────────────────────────────────────────────────────────────\n\n/** Attention buckets surfaced on roster cards. */\nexport const memberSegmentSchema = z.enum(['expiring', 'new', 'at-risk']);\nexport type MemberSegment = z.infer<typeof memberSegmentSchema>;\n\n// ─── Roster item (list projection) ───────────────────────────────────────────\n\nexport const memberRosterItemSchema = z.object({\n /** The gym record id — always present, even before the member claims. */\n memberProfileId: objectIdSchema,\n /** Null until claimed; the roster shows an \"Invited\" pill in that state. */\n userId: objectIdSchema.nullable(),\n recordStatus: memberRecordStatusSchema,\n membershipId: objectIdSchema.nullable(),\n memberCode: z.string(),\n name: z.string(),\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n /**\n * Contact details, on the ROSTER row rather than only the detail record.\n *\n * The card's whole job when someone is overdue is to let the owner chase it\n * without leaving the list — call, WhatsApp, email. Those are `tel:` /\n * `wa.me` / `mailto:` links, so the row cannot build them without the raw\n * values, and fetching the full member just to dial them would be one\n * request per card.\n *\n * Nullable because an invited record may have only one of the two: the gym\n * creates it with whatever it has, and the member fills in the rest on claim.\n * Phone is RAW 10 digits — `+91` is display-only, never stored.\n */\n phone: z.string().nullable(),\n email: z.email().nullable(),\n planName: z.string().nullable(),\n tier: planTierSchema.nullable(),\n status: membershipStatusSchema.nullable(),\n endDate: isoDateSchema.nullable(),\n lastVisitAt: isoDateTimeSchema.nullable(),\n balanceDueInr: priceInrSchema,\n overdueDays: z.number().int().nullable(),\n segments: z.array(memberSegmentSchema),\n\n /**\n * When the gym record was created — NOT when the person claimed it. The\n * roster's \"Joined Date\" filter groups by how long someone has been a member\n * of the gym, which starts at the desk, not at the login.\n */\n joinedAt: isoDateTimeSchema,\n\n /**\n * Check-ins in the trailing 14 days, used by the roster's attendance filters\n * to separate a no-show from an irregular from a regular.\n *\n * Counted rather than derived from `lastVisitAt`, because the two answer\n * different questions: someone who came once yesterday and someone who comes\n * daily share a last visit but are not the same member to chase.\n */\n visits14d: z.number().int().nonnegative(),\n\n /** Assigned trainer, so the roster can be filtered down to one trainer's people. */\n trainerId: objectIdSchema.nullable(),\n trainerName: z.string().nullable(),\n});\n\n/** Trailing window the roster's attendance filters and `visits14d` are measured over. */\nexport const ATTENDANCE_WINDOW_DAYS = 14;\nexport type MemberRosterItem = z.infer<typeof memberRosterItemSchema>;\n\n/** Header counts for the roster. */\nexport const memberListSummarySchema = z.object({\n total: z.number().int().nonnegative(),\n active: z.number().int().nonnegative(),\n expiring: z.number().int().nonnegative(),\n inactive: z.number().int().nonnegative(),\n});\nexport type MemberListSummary = z.infer<typeof memberListSummarySchema>;\n\n// ─── List query ──────────────────────────────────────────────────────────────\n\nexport const memberFilterSchema = z.enum(['all', 'active', 'inactive', 'verified']);\nexport type MemberFilter = z.infer<typeof memberFilterSchema>;\n\nexport const memberSortSchema = z.enum(['recent', 'az', 'expiry', 'dues']);\nexport type MemberSort = z.infer<typeof memberSortSchema>;\n\nexport const memberListQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema.optional(),\n filter: memberFilterSchema.default('all'),\n sort: memberSortSchema.default('recent'),\n tier: planTierSchema.optional(),\n q: optionalTrimmedString(120),\n});\nexport type MemberListQuery = z.infer<typeof memberListQuerySchema>;\n\n// ─── Detail projection ───────────────────────────────────────────────────────\n\n/** The identity slice a projection pulls from the auth `User`. */\nexport const memberUserRefSchema = z.object({\n /** Null while the record is still `invited` (no central-auth account linked). */\n id: objectIdSchema.nullable(),\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n});\nexport type MemberUserRef = z.infer<typeof memberUserRefSchema>;\n\nexport const memberDetailSchema = z.object({\n memberProfileId: objectIdSchema,\n recordStatus: memberRecordStatusSchema,\n /**\n * Identity from central auth once claimed. Before that the gym still knows the\n * person (name / contact captured at the desk), so this carries the invited\n * details with `id: null`.\n */\n user: memberUserRefSchema,\n memberCode: z.string(),\n gender: genderSchema.nullable(),\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n address: addressSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n /**\n * Last four digits of the member's Aadhaar, when one was recorded at the desk.\n * The full number is never stored (see `member-profile`), so this is all the\n * owner console can ever show — enough to match against the physical card the\n * member presents, useless to anyone who sees the screen.\n */\n aadhaarLast4: z.string().nullable(),\n /**\n * The scanned ID the desk took, if any. Carried on the detail projection so\n * the owner's edit form can round-trip it: a form that cannot see a stored\n * value sends back a blank one, and blank means \"clear it\".\n */\n idProofUrl: z.string().nullable(),\n joinedAt: isoDateTimeSchema,\n lastVisitAt: isoDateTimeSchema.nullable(),\n assignedWorkoutPlanId: objectIdSchema.nullable(),\n assignedTrainerId: objectIdSchema.nullable(),\n /**\n * The member's current membership, in FULL rather than by id.\n *\n * The profile screen shows the plan name, its billing cycle, the lifecycle\n * status pill and the expiry date all at once, and every one of those lives\n * here. Sending only the id forced the owner console to re-query the roster\n * for the same member just to read the plan columns back, and to match the\n * plan by NAME to find its perk icons — both of which this removes.\n */\n membership: membershipSchema.nullable(),\n /**\n * Still owed on the current term: its price less what has been recorded as\n * collected. Zero when settled, when the membership was cancelled, or when\n * there is no membership at all. Same definition as the roster's column, so\n * the profile and the list can never disagree about who owes money.\n */\n balanceDueInr: z.number().int().nonnegative(),\n});\nexport type MemberDetail = z.infer<typeof memberDetailSchema>;\n\n// ─── Add member (provisioning) ───────────────────────────────────────────────\n\nexport const createMemberBodySchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n email: z.email('A valid email is required'),\n phone: phoneSchema,\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n bloodGroup: bloodGroupSchema.optional(),\n address: addressSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(5).default([]),\n idProofUrl: optionalTrimmedString(2000),\n avatarUrl: optionalTrimmedString(2000),\n assignedTrainerId: objectIdSchema.optional(),\n /** Optionally assign a plan in the same step. */\n initialPlan: assignMembershipBodySchema.optional(),\n});\nexport type CreateMemberBody = z.infer<typeof createMemberBodySchema>;\n\n/**\n * Edit a member's record from the owner console.\n *\n * Every field is optional — the console sends what the form holds, and a field\n * left out is left alone.\n *\n * ## What the gym may and may not change\n * `firstName` / `lastName` / `email` / `phone` are the INVITE details, and the\n * server accepts them only while the record is unclaimed. Once the member has\n * linked their central-auth account, that account is the source of truth for\n * their identity and is shared with the other products — a gym must not be able\n * to rewrite someone's name or email out from under them. The server ignores\n * those four on a claimed record rather than failing the whole request, so an\n * owner correcting a blood group is never blocked by a name they cannot change.\n *\n * Nullable fields (`dob`, `bloodGroup`, `address`, `assignedTrainerId`) accept\n * `null` to CLEAR a value, which `undefined` cannot express.\n */\nexport const updateMemberBodySchema = createMemberBodySchema\n .omit({\n initialPlan: true,\n dob: true,\n bloodGroup: true,\n address: true,\n assignedTrainerId: true,\n })\n .partial()\n .extend({\n dob: isoDateSchema.nullish(),\n bloodGroup: bloodGroupSchema.nullish(),\n address: addressSchema.nullish(),\n assignedTrainerId: objectIdSchema.nullish(),\n /** Last four digits only — the full number is never stored. */\n aadhaarLast4: z\n .string()\n .trim()\n .regex(/^\\d{4}$/, 'Enter the last four digits of the Aadhaar number')\n .nullish(),\n });\nexport type UpdateMemberBody = z.infer<typeof updateMemberBodySchema>;\n\n// ─── Invite & claim ──────────────────────────────────────────────────────────\n\n/**\n * The invite a gym hands to a new member.\n *\n * GymMonk never mints credentials for anyone: central auth is the SSOT for\n * identity, and an account there is shared across products. So adding a member\n * creates a gym record only, plus an unguessable invite code. The member\n * registers/logs in themselves through central auth (exactly as in jansathi) and\n * then CLAIMS the record. The owner shares `url` over WhatsApp or shows the QR.\n */\nexport const memberInviteSchema = z.object({\n /** Single-use, unguessable. Regenerating an invite invalidates the old one. */\n code: z.string(),\n /** Absolute claim URL the owner shares (also encoded into the QR). */\n url: z.string(),\n /** The address the member MUST register with for the claim to succeed. */\n email: z.string(),\n /** WhatsApp number to share on, when the gym recorded one. */\n phone: z.string().nullable(),\n expiresAt: isoDateTimeSchema,\n});\nexport type MemberInvite = z.infer<typeof memberInviteSchema>;\n\n/** Result of adding a member: the gym record + the invite to hand over. */\nexport const provisionResultSchema = z.object({\n /** The new member record (NOT a central-auth user — none is created). */\n memberProfileId: objectIdSchema,\n code: z.string(),\n invite: memberInviteSchema,\n /**\n * The membership created from `initialPlan`, or `null`.\n *\n * Null means one of two DIFFERENT things, and the caller can tell them apart\n * because it knows whether it sent `initialPlan`: no plan was chosen, or the\n * chosen plan could not be assigned. Adding a member never fails over the\n * second case — the person is on the roster either way — so this is how the\n * owner finds out the plan still needs assigning rather than assuming it took.\n */\n membership: membershipSchema.nullable(),\n});\nexport type ProvisionResult = z.infer<typeof provisionResultSchema>;\n\n/**\n * What the claim screen shows BEFORE the member authenticates, so they know\n * which address to register with. Public (the code is the bearer secret), and\n * deliberately minimal — gym name, member code, and the expected email.\n */\nexport const invitePreviewSchema = z.object({\n gymName: z.string(),\n memberCode: z.string(),\n /** Shown verbatim: the member must know exactly which address to use. */\n email: z.string(),\n expiresAt: isoDateTimeSchema,\n /** True once claimed / expired, so the screen can explain why it failed. */\n claimed: z.boolean(),\n expired: z.boolean(),\n});\nexport type InvitePreview = z.infer<typeof invitePreviewSchema>;\n\n/**\n * Claim an invite as the authenticated caller. The server links the record only\n * when the code is valid AND the caller's central-auth email matches the address\n * the invite was issued to.\n */\nexport const claimInviteBodySchema = z.object({\n code: z.string().trim().min(1),\n});\nexport type ClaimInviteBody = z.infer<typeof claimInviteBodySchema>;\n\n// ─── Mutations ───────────────────────────────────────────────────────────────\n\nexport const setMemberStatusBodySchema = z.object({ active: z.boolean() });\nexport type SetMemberStatusBody = z.infer<typeof setMemberStatusBodySchema>;\n\nexport const assignTrainerBodySchema = z.object({\n trainerId: objectIdSchema.nullable(),\n});\nexport type AssignTrainerBody = z.infer<typeof assignTrainerBodySchema>;\n\nexport const assignWorkoutPlanBodySchema = z.object({\n planId: objectIdSchema.nullable(),\n});\nexport type AssignWorkoutPlanBody = z.infer<typeof assignWorkoutPlanBodySchema>;\n\n/** Remove a member from the gym — echoes the member code to confirm. */\nexport const deleteMemberBodySchema = z.object({\n confirmationCode: z.string().trim().min(1),\n});\nexport type DeleteMemberBody = z.infer<typeof deleteMemberBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"member.js","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Member management (owner console)\n * ==================================================\n * Owner-facing roster + detail projections (User × MemberProfile × Membership)\n * and the \"add member\" provisioning payload. Adding a member provisions a real\n * central-auth account, so the result carries the login email + temp password.\n *\n * @module gymmonk-schema/member\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n phoneSchema,\n priceInrSchema,\n} from './common.js';\nimport {\n emergencyContactSchema,\n MAX_EMERGENCY_CONTACTS,\n memberRecordStatusSchema,\n} from './member-profile.js';\nimport {\n assignMembershipBodySchema,\n membershipSchema,\n membershipStatusSchema,\n} from './membership.js';\nimport { planTierSchema } from './membership-plan.js';\nimport { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Segments ────────────────────────────────────────────────────────────────\n\n/** Attention buckets surfaced on roster cards. */\nexport const memberSegmentSchema = z.enum(['expiring', 'new', 'at-risk']);\nexport type MemberSegment = z.infer<typeof memberSegmentSchema>;\n\n// ─── Roster item (list projection) ───────────────────────────────────────────\n\nexport const memberRosterItemSchema = z.object({\n /** The gym record id — always present, even before the member claims. */\n memberProfileId: objectIdSchema,\n /** Null until claimed; the roster shows an \"Invited\" pill in that state. */\n userId: objectIdSchema.nullable(),\n recordStatus: memberRecordStatusSchema,\n membershipId: objectIdSchema.nullable(),\n memberCode: z.string(),\n name: z.string(),\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n /**\n * Contact details, on the ROSTER row rather than only the detail record.\n *\n * The card's whole job when someone is overdue is to let the owner chase it\n * without leaving the list — call, WhatsApp, email. Those are `tel:` /\n * `wa.me` / `mailto:` links, so the row cannot build them without the raw\n * values, and fetching the full member just to dial them would be one\n * request per card.\n *\n * Nullable because an invited record may have only one of the two: the gym\n * creates it with whatever it has, and the member fills in the rest on claim.\n * Phone is RAW 10 digits — `+91` is display-only, never stored.\n */\n phone: z.string().nullable(),\n email: z.email().nullable(),\n planName: z.string().nullable(),\n tier: planTierSchema.nullable(),\n status: membershipStatusSchema.nullable(),\n endDate: isoDateSchema.nullable(),\n lastVisitAt: isoDateTimeSchema.nullable(),\n balanceDueInr: priceInrSchema,\n overdueDays: z.number().int().nullable(),\n segments: z.array(memberSegmentSchema),\n\n /**\n * When the gym record was created — NOT when the person claimed it. The\n * roster's \"Joined Date\" filter groups by how long someone has been a member\n * of the gym, which starts at the desk, not at the login.\n */\n joinedAt: isoDateTimeSchema,\n\n /**\n * Check-ins in the trailing 14 days, used by the roster's attendance filters\n * to separate a no-show from an irregular from a regular.\n *\n * Counted rather than derived from `lastVisitAt`, because the two answer\n * different questions: someone who came once yesterday and someone who comes\n * daily share a last visit but are not the same member to chase.\n */\n visits14d: z.number().int().nonnegative(),\n\n /** Assigned trainer, so the roster can be filtered down to one trainer's people. */\n trainerId: objectIdSchema.nullable(),\n trainerName: z.string().nullable(),\n});\n\n/** Trailing window the roster's attendance filters and `visits14d` are measured over. */\nexport const ATTENDANCE_WINDOW_DAYS = 14;\nexport type MemberRosterItem = z.infer<typeof memberRosterItemSchema>;\n\n/** Header counts for the roster. */\nexport const memberListSummarySchema = z.object({\n total: z.number().int().nonnegative(),\n active: z.number().int().nonnegative(),\n expiring: z.number().int().nonnegative(),\n inactive: z.number().int().nonnegative(),\n});\nexport type MemberListSummary = z.infer<typeof memberListSummarySchema>;\n\n// ─── List query ──────────────────────────────────────────────────────────────\n\nexport const memberFilterSchema = z.enum(['all', 'active', 'inactive', 'verified']);\nexport type MemberFilter = z.infer<typeof memberFilterSchema>;\n\nexport const memberSortSchema = z.enum(['recent', 'az', 'expiry', 'dues']);\nexport type MemberSort = z.infer<typeof memberSortSchema>;\n\nexport const memberListQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema.optional(),\n filter: memberFilterSchema.default('all'),\n sort: memberSortSchema.default('recent'),\n tier: planTierSchema.optional(),\n q: optionalTrimmedString(120),\n});\nexport type MemberListQuery = z.infer<typeof memberListQuerySchema>;\n\n// ─── Detail projection ───────────────────────────────────────────────────────\n\n/** The identity slice a projection pulls from the auth `User`. */\nexport const memberUserRefSchema = z.object({\n /** Null while the record is still `invited` (no central-auth account linked). */\n id: objectIdSchema.nullable(),\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n});\nexport type MemberUserRef = z.infer<typeof memberUserRefSchema>;\n\nexport const memberDetailSchema = z.object({\n memberProfileId: objectIdSchema,\n recordStatus: memberRecordStatusSchema,\n /**\n * Identity from central auth once claimed. Before that the gym still knows the\n * person (name / contact captured at the desk), so this carries the invited\n * details with `id: null`.\n */\n user: memberUserRefSchema,\n memberCode: z.string(),\n gender: genderSchema.nullable(),\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n address: addressSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n /**\n * Last four digits of the member's Aadhaar, when one was recorded at the desk.\n * The full number is never stored (see `member-profile`), so this is all the\n * owner console can ever show — enough to match against the physical card the\n * member presents, useless to anyone who sees the screen.\n */\n aadhaarLast4: z.string().nullable(),\n /**\n * The scanned ID the desk took, if any. Carried on the detail projection so\n * the owner's edit form can round-trip it: a form that cannot see a stored\n * value sends back a blank one, and blank means \"clear it\".\n */\n idProofUrl: z.string().nullable(),\n joinedAt: isoDateTimeSchema,\n lastVisitAt: isoDateTimeSchema.nullable(),\n assignedWorkoutPlanId: objectIdSchema.nullable(),\n assignedTrainerId: objectIdSchema.nullable(),\n /**\n * The member's current membership, in FULL rather than by id.\n *\n * The profile screen shows the plan name, its billing cycle, the lifecycle\n * status pill and the expiry date all at once, and every one of those lives\n * here. Sending only the id forced the owner console to re-query the roster\n * for the same member just to read the plan columns back, and to match the\n * plan by NAME to find its perk icons — both of which this removes.\n */\n membership: membershipSchema.nullable(),\n /**\n * Still owed on the current term: its price less what has been recorded as\n * collected. Zero when settled, when the membership was cancelled, or when\n * there is no membership at all. Same definition as the roster's column, so\n * the profile and the list can never disagree about who owes money.\n */\n balanceDueInr: z.number().int().nonnegative(),\n});\nexport type MemberDetail = z.infer<typeof memberDetailSchema>;\n\n// ─── Add member (provisioning) ───────────────────────────────────────────────\n\nexport const createMemberBodySchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n email: z.email('A valid email is required'),\n phone: phoneSchema,\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n bloodGroup: bloodGroupSchema.optional(),\n address: addressSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\n idProofUrl: optionalTrimmedString(2000),\n avatarUrl: optionalTrimmedString(2000),\n assignedTrainerId: objectIdSchema.optional(),\n /** Optionally assign a plan in the same step. */\n initialPlan: assignMembershipBodySchema.optional(),\n});\nexport type CreateMemberBody = z.infer<typeof createMemberBodySchema>;\n\n/**\n * Edit a member's record from the owner console.\n *\n * Every field is optional — the console sends what the form holds, and a field\n * left out is left alone.\n *\n * ## What the gym may and may not change\n * `firstName` / `lastName` / `email` / `phone` are the INVITE details, and the\n * server accepts them only while the record is unclaimed. Once the member has\n * linked their central-auth account, that account is the source of truth for\n * their identity and is shared with the other products — a gym must not be able\n * to rewrite someone's name or email out from under them. The server ignores\n * those four on a claimed record rather than failing the whole request, so an\n * owner correcting a blood group is never blocked by a name they cannot change.\n *\n * Nullable fields (`dob`, `bloodGroup`, `address`, `assignedTrainerId`) accept\n * `null` to CLEAR a value, which `undefined` cannot express.\n */\nexport const updateMemberBodySchema = createMemberBodySchema\n .omit({\n initialPlan: true,\n dob: true,\n bloodGroup: true,\n address: true,\n assignedTrainerId: true,\n })\n .partial()\n .extend({\n dob: isoDateSchema.nullish(),\n bloodGroup: bloodGroupSchema.nullish(),\n address: addressSchema.nullish(),\n assignedTrainerId: objectIdSchema.nullish(),\n /** Last four digits only — the full number is never stored. */\n aadhaarLast4: z\n .string()\n .trim()\n .regex(/^\\d{4}$/, 'Enter the last four digits of the Aadhaar number')\n .nullish(),\n });\nexport type UpdateMemberBody = z.infer<typeof updateMemberBodySchema>;\n\n// ─── Invite & claim ──────────────────────────────────────────────────────────\n\n/**\n * The invite a gym hands to a new member.\n *\n * GymMonk never mints credentials for anyone: central auth is the SSOT for\n * identity, and an account there is shared across products. So adding a member\n * creates a gym record only, plus an unguessable invite code. The member\n * registers/logs in themselves through central auth (exactly as in jansathi) and\n * then CLAIMS the record. The owner shares `url` over WhatsApp or shows the QR.\n */\nexport const memberInviteSchema = z.object({\n /** Single-use, unguessable. Regenerating an invite invalidates the old one. */\n code: z.string(),\n /** Absolute claim URL the owner shares (also encoded into the QR). */\n url: z.string(),\n /** The address the member MUST register with for the claim to succeed. */\n email: z.string(),\n /** WhatsApp number to share on, when the gym recorded one. */\n phone: z.string().nullable(),\n expiresAt: isoDateTimeSchema,\n});\nexport type MemberInvite = z.infer<typeof memberInviteSchema>;\n\n/** Result of adding a member: the gym record + the invite to hand over. */\nexport const provisionResultSchema = z.object({\n /** The new member record (NOT a central-auth user — none is created). */\n memberProfileId: objectIdSchema,\n code: z.string(),\n invite: memberInviteSchema,\n /**\n * The membership created from `initialPlan`, or `null`.\n *\n * Null means one of two DIFFERENT things, and the caller can tell them apart\n * because it knows whether it sent `initialPlan`: no plan was chosen, or the\n * chosen plan could not be assigned. Adding a member never fails over the\n * second case — the person is on the roster either way — so this is how the\n * owner finds out the plan still needs assigning rather than assuming it took.\n */\n membership: membershipSchema.nullable(),\n});\nexport type ProvisionResult = z.infer<typeof provisionResultSchema>;\n\n/**\n * What the claim screen shows BEFORE the member authenticates, so they know\n * which address to register with. Public (the code is the bearer secret), and\n * deliberately minimal — gym name, member code, and the expected email.\n */\nexport const invitePreviewSchema = z.object({\n gymName: z.string(),\n memberCode: z.string(),\n /** Shown verbatim: the member must know exactly which address to use. */\n email: z.string(),\n expiresAt: isoDateTimeSchema,\n /** True once claimed / expired, so the screen can explain why it failed. */\n claimed: z.boolean(),\n expired: z.boolean(),\n});\nexport type InvitePreview = z.infer<typeof invitePreviewSchema>;\n\n/**\n * Claim an invite as the authenticated caller. The server links the record only\n * when the code is valid AND the caller's central-auth email matches the address\n * the invite was issued to.\n */\nexport const claimInviteBodySchema = z.object({\n code: z.string().trim().min(1),\n});\nexport type ClaimInviteBody = z.infer<typeof claimInviteBodySchema>;\n\n// ─── Mutations ───────────────────────────────────────────────────────────────\n\nexport const setMemberStatusBodySchema = z.object({ active: z.boolean() });\nexport type SetMemberStatusBody = z.infer<typeof setMemberStatusBodySchema>;\n\nexport const assignTrainerBodySchema = z.object({\n trainerId: objectIdSchema.nullable(),\n});\nexport type AssignTrainerBody = z.infer<typeof assignTrainerBodySchema>;\n\nexport const assignWorkoutPlanBodySchema = z.object({\n planId: objectIdSchema.nullable(),\n});\nexport type AssignWorkoutPlanBody = z.infer<typeof assignWorkoutPlanBodySchema>;\n\n/** Remove a member from the gym — echoes the member code to confirm. */\nexport const deleteMemberBodySchema = z.object({\n confirmationCode: z.string().trim().min(1),\n});\nexport type DeleteMemberBody = z.infer<typeof deleteMemberBodySchema>;\n"]}
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { z } from 'zod';
|
|
30
30
|
import { type EmergencyContactRow } from './add-person-form.js';
|
|
31
|
-
import { type Address } from './shared.js';
|
|
32
31
|
/**
|
|
33
32
|
* Name, date of birth, gender and phone — the identity half of the profile.
|
|
34
33
|
*
|
|
@@ -64,13 +63,33 @@ export type MemberPersonalFormValues = {
|
|
|
64
63
|
phone: string;
|
|
65
64
|
};
|
|
66
65
|
/**
|
|
67
|
-
* Blood group
|
|
66
|
+
* Blood group and the emergency contact. Nothing else.
|
|
67
|
+
*
|
|
68
|
+
* ── Why it is only these two ────────────────────────────────────────────────
|
|
69
|
+
* This page also used to ask the member for their address, an ID-proof upload
|
|
70
|
+
* and the last four digits of their Aadhaar. Those are DESK-COLLECTED facts:
|
|
71
|
+
* the onboarding wizard never asks a member for any of them, and the owner
|
|
72
|
+
* console does — an identity document a person types about themselves is not
|
|
73
|
+
* evidence of anything, so the gym recording it is the only version that
|
|
74
|
+
* means something. They remain on `upsertMemberProfileBodySchema` because the
|
|
75
|
+
* owner still writes them; they are simply not the member's to fill in.
|
|
76
|
+
*
|
|
77
|
+
* That leaves the member's own page asking for exactly what the wizard asked
|
|
78
|
+
* for, which is the point: the two surfaces collect one fact set, so opening
|
|
79
|
+
* the editor after onboarding no longer presents three boxes nobody has an
|
|
80
|
+
* answer for.
|
|
68
81
|
*
|
|
69
82
|
* Blank is allowed on everything except a contact row that has been touched:
|
|
70
83
|
* this is a record a member fills in over time, not a gate. `bloodGroup` is the
|
|
71
84
|
* CANONICAL enum value (ASCII hyphen) — the picker displays a typographic minus
|
|
72
85
|
* but must never emit one, because that string is not a `BloodGroup` and the
|
|
73
86
|
* API would reject it.
|
|
87
|
+
*
|
|
88
|
+
* `contacts` stays an ARRAY at exactly one entry rather than becoming a single
|
|
89
|
+
* object. The stored shape is an array on the profile, the staff record and
|
|
90
|
+
* the wire, so collapsing it here would put a translation step between this
|
|
91
|
+
* form and every one of them for no gain — and `MAX_EMERGENCY_CONTACTS` is
|
|
92
|
+
* where "exactly one" is stated, once.
|
|
74
93
|
*/
|
|
75
94
|
export declare const memberLegalFormSchema: z.ZodObject<{
|
|
76
95
|
bloodGroup: z.ZodUnion<[z.ZodEnum<{
|
|
@@ -83,45 +102,6 @@ export declare const memberLegalFormSchema: z.ZodObject<{
|
|
|
83
102
|
"AB+": "AB+";
|
|
84
103
|
"AB-": "AB-";
|
|
85
104
|
}>, z.ZodLiteral<"">]>;
|
|
86
|
-
address: z.ZodNullable<z.ZodObject<{
|
|
87
|
-
country: z.ZodObject<{
|
|
88
|
-
id: z.ZodString;
|
|
89
|
-
name: z.ZodString;
|
|
90
|
-
}, z.core.$strip>;
|
|
91
|
-
state: z.ZodObject<{
|
|
92
|
-
id: z.ZodString;
|
|
93
|
-
name: z.ZodString;
|
|
94
|
-
}, z.core.$strip>;
|
|
95
|
-
district: z.ZodOptional<z.ZodObject<{
|
|
96
|
-
id: z.ZodString;
|
|
97
|
-
name: z.ZodString;
|
|
98
|
-
}, z.core.$strip>>;
|
|
99
|
-
locality: z.ZodOptional<z.ZodObject<{
|
|
100
|
-
id: z.ZodString;
|
|
101
|
-
name: z.ZodString;
|
|
102
|
-
type: z.ZodEnum<{
|
|
103
|
-
SUB_DISTRICT: "SUB_DISTRICT";
|
|
104
|
-
BLOCK: "BLOCK";
|
|
105
|
-
GRAM_PANCHAYAT: "GRAM_PANCHAYAT";
|
|
106
|
-
VILLAGE: "VILLAGE";
|
|
107
|
-
HAMLET: "HAMLET";
|
|
108
|
-
URBAN_BODY: "URBAN_BODY";
|
|
109
|
-
URBAN_WARD: "URBAN_WARD";
|
|
110
|
-
LOCALITY: "LOCALITY";
|
|
111
|
-
}>;
|
|
112
|
-
}, z.core.$strip>>;
|
|
113
|
-
line1: z.ZodString;
|
|
114
|
-
landmark: z.ZodPreprocess<z.ZodOptional<z.ZodString>>;
|
|
115
|
-
pincode: z.ZodString;
|
|
116
|
-
ancestry: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
117
|
-
coords: z.ZodOptional<z.ZodObject<{
|
|
118
|
-
lat: z.ZodNumber;
|
|
119
|
-
lng: z.ZodNumber;
|
|
120
|
-
source: z.ZodString;
|
|
121
|
-
}, z.core.$strip>>;
|
|
122
|
-
}, z.core.$strip>>;
|
|
123
|
-
idProofUrl: z.ZodString;
|
|
124
|
-
aadhaarLast4: z.ZodString;
|
|
125
105
|
contacts: z.ZodArray<z.ZodObject<{
|
|
126
106
|
name: z.ZodString;
|
|
127
107
|
relationship: z.ZodString;
|
|
@@ -130,9 +110,6 @@ export declare const memberLegalFormSchema: z.ZodObject<{
|
|
|
130
110
|
}, z.core.$strip>;
|
|
131
111
|
export type MemberLegalFormValues = {
|
|
132
112
|
bloodGroup: string;
|
|
133
|
-
address: Address | null;
|
|
134
|
-
idProofUrl: string;
|
|
135
|
-
aadhaarLast4: string;
|
|
136
113
|
contacts: EmergencyContactRow[];
|
|
137
114
|
};
|
|
138
115
|
//# sourceMappingURL=profile-edit-form.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-edit-form.d.ts","sourceRoot":"","sources":["../src/profile-edit-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,
|
|
1
|
+
{"version":3,"file":"profile-edit-form.d.ts","sourceRoot":"","sources":["../src/profile-edit-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,KAAK,mBAAmB,EAA6B,MAAM,sBAAsB,CAAC;AAQ3F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB;;;;;;;;;;;;iBAUN,CAAC;AAEhC,wEAAwE;AACxE,MAAM,MAAM,wBAAwB,GAAG;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAIF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;iBAGhC,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CACjC,CAAC"}
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
* @module gymmonk-schema/profile-edit-form
|
|
28
28
|
*/
|
|
29
29
|
import { z } from 'zod';
|
|
30
|
-
import { emergencyContactRowSchema
|
|
30
|
+
import { emergencyContactRowSchema } from './add-person-form.js';
|
|
31
31
|
import { PHONE_REGEX } from './common.js';
|
|
32
|
+
import { MAX_EMERGENCY_CONTACTS } from './member-profile.js';
|
|
32
33
|
import { refineMemberDob } from './onboarding-form.js';
|
|
33
|
-
import {
|
|
34
|
+
import { bloodGroupSchema, genderSchema } from './shared.js';
|
|
34
35
|
// ─── Personal details ────────────────────────────────────────────────────────
|
|
35
36
|
/**
|
|
36
37
|
* Name, date of birth, gender and phone — the identity half of the profile.
|
|
@@ -56,28 +57,36 @@ export const memberPersonalFormSchema = z
|
|
|
56
57
|
.superRefine(refineMemberDob);
|
|
57
58
|
// ─── Legal & emergency ───────────────────────────────────────────────────────
|
|
58
59
|
/**
|
|
59
|
-
* Blood group
|
|
60
|
+
* Blood group and the emergency contact. Nothing else.
|
|
61
|
+
*
|
|
62
|
+
* ── Why it is only these two ────────────────────────────────────────────────
|
|
63
|
+
* This page also used to ask the member for their address, an ID-proof upload
|
|
64
|
+
* and the last four digits of their Aadhaar. Those are DESK-COLLECTED facts:
|
|
65
|
+
* the onboarding wizard never asks a member for any of them, and the owner
|
|
66
|
+
* console does — an identity document a person types about themselves is not
|
|
67
|
+
* evidence of anything, so the gym recording it is the only version that
|
|
68
|
+
* means something. They remain on `upsertMemberProfileBodySchema` because the
|
|
69
|
+
* owner still writes them; they are simply not the member's to fill in.
|
|
70
|
+
*
|
|
71
|
+
* That leaves the member's own page asking for exactly what the wizard asked
|
|
72
|
+
* for, which is the point: the two surfaces collect one fact set, so opening
|
|
73
|
+
* the editor after onboarding no longer presents three boxes nobody has an
|
|
74
|
+
* answer for.
|
|
60
75
|
*
|
|
61
76
|
* Blank is allowed on everything except a contact row that has been touched:
|
|
62
77
|
* this is a record a member fills in over time, not a gate. `bloodGroup` is the
|
|
63
78
|
* CANONICAL enum value (ASCII hyphen) — the picker displays a typographic minus
|
|
64
79
|
* but must never emit one, because that string is not a `BloodGroup` and the
|
|
65
80
|
* API would reject it.
|
|
81
|
+
*
|
|
82
|
+
* `contacts` stays an ARRAY at exactly one entry rather than becoming a single
|
|
83
|
+
* object. The stored shape is an array on the profile, the staff record and
|
|
84
|
+
* the wire, so collapsing it here would put a translation step between this
|
|
85
|
+
* form and every one of them for no gain — and `MAX_EMERGENCY_CONTACTS` is
|
|
86
|
+
* where "exactly one" is stated, once.
|
|
66
87
|
*/
|
|
67
88
|
export const memberLegalFormSchema = z.object({
|
|
68
89
|
bloodGroup: bloodGroupSchema.or(z.literal('')),
|
|
69
|
-
/** Atomic: the location drawer hands back a complete address or nothing. */
|
|
70
|
-
address: addressSchema.nullable(),
|
|
71
|
-
/** GCS URL — the document uploads on pick, so this is never a file. */
|
|
72
|
-
idProofUrl: z.string(),
|
|
73
|
-
/**
|
|
74
|
-
* Display-only, and the reason it is asked at all: the profile shows an
|
|
75
|
-
* "Aadhaar xxxx xxxx 1234" row that nothing on the member's side could set.
|
|
76
|
-
*/
|
|
77
|
-
aadhaarLast4: z
|
|
78
|
-
.string()
|
|
79
|
-
.trim()
|
|
80
|
-
.refine((v) => v === '' || /^\d{4}$/.test(v), 'Enter the last 4 digits'),
|
|
81
90
|
contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),
|
|
82
91
|
});
|
|
83
92
|
//# sourceMappingURL=profile-edit-form.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-edit-form.js","sourceRoot":"","sources":["../src/profile-edit-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,
|
|
1
|
+
{"version":3,"file":"profile-edit-form.js","sourceRoot":"","sources":["../src/profile-edit-form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAA4B,yBAAyB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,gFAAgF;AAEhF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC;KACtC,MAAM,CAAC;IACN,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,wBAAwB,CAAC;IAC7D,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE;IAC3B,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,eAAe,CAAC;IAC1C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC;IAC9C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,sBAAsB,CAAC;IACrD,MAAM,EAAE,YAAY,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,EAAE,oBAAoB,EAAE,CAAC;IAC5F,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,sCAAsC,CAAC;CAC7E,CAAC;KACD,WAAW,CAAC,eAAe,CAAC,CAAC;AAahC,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,UAAU,EAAE,gBAAgB,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9C,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC;CACzE,CAAC,CAAC","sourcesContent":["/**\n * gymmonk-schema — member \"edit my profile\" FORM schemas\n * ======================================================\n * The member's own Me tab splits their record across three sub-pages: Personal\n * Details, Body Measurements, Legal & Emergency. This module holds the\n * validation contract for the two that ask for the same facts the onboarding\n * wizard asked for.\n *\n * ── Why these exist ─────────────────────────────────────────────────────────\n * The edit pages used to have NO validation at all — no resolver, free-text\n * boxes for date of birth and blood group — while the wizard that first\n * collected those exact fields validated every one of them. The mismatch was\n * not cosmetic: the save path drops any value it cannot parse and still reports\n * \"Profile updated\", so a member could retype their date of birth, be told it\n * saved, and lose it.\n *\n * So the rules are stated ONCE, here, over the same three-cell date and the\n * same emergency-contact row the other surfaces use:\n *\n * `refineMemberDob` shared with the onboarding wizard\n * `emergencyContactRowSchema` shared with the owner's desk-entry forms\n *\n * Body Measurements deliberately has no schema here. Every measurement is\n * optional and blank simply drops the field, so there is nothing to enforce\n * that `bodyMetricsSchema` does not already enforce at the API.\n *\n * @module gymmonk-schema/profile-edit-form\n */\n\nimport { z } from 'zod';\nimport { type EmergencyContactRow, emergencyContactRowSchema } from './add-person-form.js';\nimport { PHONE_REGEX } from './common.js';\nimport { MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport { refineMemberDob } from './onboarding-form.js';\nimport { bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Personal details ────────────────────────────────────────────────────────\n\n/**\n * Name, date of birth, gender and phone — the identity half of the profile.\n *\n * Every rule is the wizard's rule. Gender is `requiredChoice`-shaped for the\n * same reason it is there: a member who has never recorded one arrives with\n * `null`, and that is a real state the form must be able to hold without\n * pretending they answered.\n *\n * Email is NOT here. It is the auth identity, `PUT /users/me` does not carry\n * it, and the field is rendered read-only.\n */\nexport const memberPersonalFormSchema = z\n .object({\n firstName: z.string().trim().min(1, 'First name is required'),\n lastName: z.string().trim(),\n dobDay: z.string().min(1, 'Enter the day'),\n dobMonth: z.string().min(1, 'Enter the month'),\n dobYear: z.string().length(4, 'Enter a 4-digit year'),\n gender: genderSchema.nullable().refine((v) => v !== null, { message: 'Select your gender' }),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n })\n .superRefine(refineMemberDob);\n\n/** Working shape — `gender` is null until picked, like the wizard's. */\nexport type MemberPersonalFormValues = {\n firstName: string;\n lastName: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: 'male' | 'female' | 'other' | null;\n phone: string;\n};\n\n// ─── Legal & emergency ───────────────────────────────────────────────────────\n\n/**\n * Blood group and the emergency contact. Nothing else.\n *\n * ── Why it is only these two ────────────────────────────────────────────────\n * This page also used to ask the member for their address, an ID-proof upload\n * and the last four digits of their Aadhaar. Those are DESK-COLLECTED facts:\n * the onboarding wizard never asks a member for any of them, and the owner\n * console does — an identity document a person types about themselves is not\n * evidence of anything, so the gym recording it is the only version that\n * means something. They remain on `upsertMemberProfileBodySchema` because the\n * owner still writes them; they are simply not the member's to fill in.\n *\n * That leaves the member's own page asking for exactly what the wizard asked\n * for, which is the point: the two surfaces collect one fact set, so opening\n * the editor after onboarding no longer presents three boxes nobody has an\n * answer for.\n *\n * Blank is allowed on everything except a contact row that has been touched:\n * this is a record a member fills in over time, not a gate. `bloodGroup` is the\n * CANONICAL enum value (ASCII hyphen) — the picker displays a typographic minus\n * but must never emit one, because that string is not a `BloodGroup` and the\n * API would reject it.\n *\n * `contacts` stays an ARRAY at exactly one entry rather than becoming a single\n * object. The stored shape is an array on the profile, the staff record and\n * the wire, so collapsing it here would put a translation step between this\n * form and every one of them for no gain — and `MAX_EMERGENCY_CONTACTS` is\n * where \"exactly one\" is stated, once.\n */\nexport const memberLegalFormSchema = z.object({\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n});\n\nexport type MemberLegalFormValues = {\n bloodGroup: string;\n contacts: EmergencyContactRow[];\n};\n"]}
|
package/dist/session.d.ts
CHANGED
|
@@ -130,6 +130,32 @@ export declare const attendanceStatusSchema: z.ZodEnum<{
|
|
|
130
130
|
rest: "rest";
|
|
131
131
|
}>;
|
|
132
132
|
export type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;
|
|
133
|
+
/**
|
|
134
|
+
* How an attendance mark came to be recorded.
|
|
135
|
+
*
|
|
136
|
+
* `scan` — the member scanned the gym's QR poster themselves.
|
|
137
|
+
* `manual` — a person put it there: an owner, manager or front desk.
|
|
138
|
+
* `auto` — nobody did. The member never scanned out and the session hit its
|
|
139
|
+
* cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).
|
|
140
|
+
*
|
|
141
|
+
* Recorded per SIDE, because the two halves of a visit routinely differ: the
|
|
142
|
+
* common case is a member who scans in and is closed out automatically. An
|
|
143
|
+
* attendance report that cannot tell a scanned departure from an assumed one is
|
|
144
|
+
* reporting a guess as a measurement.
|
|
145
|
+
*/
|
|
146
|
+
export declare const attendanceSourceSchema: z.ZodEnum<{
|
|
147
|
+
scan: "scan";
|
|
148
|
+
manual: "manual";
|
|
149
|
+
auto: "auto";
|
|
150
|
+
}>;
|
|
151
|
+
export type AttendanceSource = z.infer<typeof attendanceSourceSchema>;
|
|
152
|
+
/**
|
|
153
|
+
* How long a session may stay open before it is closed for the member.
|
|
154
|
+
*
|
|
155
|
+
* Shared with the client so the member's screen can say when their session will
|
|
156
|
+
* end, rather than having the number live only in the server that enforces it.
|
|
157
|
+
*/
|
|
158
|
+
export declare const AUTO_CHECK_OUT_HOURS = 3;
|
|
133
159
|
export declare const attendanceEntrySchema: z.ZodObject<{
|
|
134
160
|
date: z.ZodString;
|
|
135
161
|
checkInAt: z.ZodNullable<z.ZodISODateTime>;
|
|
@@ -139,6 +165,18 @@ export declare const attendanceEntrySchema: z.ZodObject<{
|
|
|
139
165
|
absent: "absent";
|
|
140
166
|
rest: "rest";
|
|
141
167
|
}>;
|
|
168
|
+
checkInSource: z.ZodNullable<z.ZodEnum<{
|
|
169
|
+
scan: "scan";
|
|
170
|
+
manual: "manual";
|
|
171
|
+
auto: "auto";
|
|
172
|
+
}>>;
|
|
173
|
+
checkOutSource: z.ZodNullable<z.ZodEnum<{
|
|
174
|
+
scan: "scan";
|
|
175
|
+
manual: "manual";
|
|
176
|
+
auto: "auto";
|
|
177
|
+
}>>;
|
|
178
|
+
checkInBy: z.ZodNullable<z.ZodString>;
|
|
179
|
+
checkOutBy: z.ZodNullable<z.ZodString>;
|
|
142
180
|
}, z.core.$strip>;
|
|
143
181
|
export type AttendanceEntry = z.infer<typeof attendanceEntrySchema>;
|
|
144
182
|
export declare const attendanceQuerySchema: z.ZodObject<{
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,mBAAmB;;;;EAA0C,CAAC;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,gBAI5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAO/F;AAID,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAOQ,CAAC;AACvC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB;;;;;;iBAEpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E,eAAO,MAAM,sBAAsB;;;;EAAwC,CAAC;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,qBAAqB
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,mBAAmB;;;;EAA0C,CAAC;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,gBAI5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAO/F;AAID,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAOQ,CAAC;AACvC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB;;;;;;iBAEpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E,eAAO,MAAM,sBAAsB;;;;EAAwC,CAAC;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB;;;;EAAqC,CAAC;AACzE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;iBAkBhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,sEAAsE;AACtE,eAAO,MAAM,wBAAwB;;;;;;;;;;;iBAOnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
package/dist/session.js
CHANGED
|
@@ -121,11 +121,45 @@ export const stopwatchActionBodySchema = z.object({
|
|
|
121
121
|
});
|
|
122
122
|
// ─── Attendance ──────────────────────────────────────────────────────────────
|
|
123
123
|
export const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);
|
|
124
|
+
/**
|
|
125
|
+
* How an attendance mark came to be recorded.
|
|
126
|
+
*
|
|
127
|
+
* `scan` — the member scanned the gym's QR poster themselves.
|
|
128
|
+
* `manual` — a person put it there: an owner, manager or front desk.
|
|
129
|
+
* `auto` — nobody did. The member never scanned out and the session hit its
|
|
130
|
+
* cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).
|
|
131
|
+
*
|
|
132
|
+
* Recorded per SIDE, because the two halves of a visit routinely differ: the
|
|
133
|
+
* common case is a member who scans in and is closed out automatically. An
|
|
134
|
+
* attendance report that cannot tell a scanned departure from an assumed one is
|
|
135
|
+
* reporting a guess as a measurement.
|
|
136
|
+
*/
|
|
137
|
+
export const attendanceSourceSchema = z.enum(['scan', 'manual', 'auto']);
|
|
138
|
+
/**
|
|
139
|
+
* How long a session may stay open before it is closed for the member.
|
|
140
|
+
*
|
|
141
|
+
* Shared with the client so the member's screen can say when their session will
|
|
142
|
+
* end, rather than having the number live only in the server that enforces it.
|
|
143
|
+
*/
|
|
144
|
+
export const AUTO_CHECK_OUT_HOURS = 3;
|
|
124
145
|
export const attendanceEntrySchema = z.object({
|
|
125
146
|
date: isoDateSchema,
|
|
126
147
|
checkInAt: isoDateTimeSchema.nullable(),
|
|
127
148
|
checkOutAt: isoDateTimeSchema.nullable(),
|
|
128
149
|
status: attendanceStatusSchema,
|
|
150
|
+
/** How each half was recorded. Null where that half has not happened. */
|
|
151
|
+
checkInSource: attendanceSourceSchema.nullable(),
|
|
152
|
+
checkOutSource: attendanceSourceSchema.nullable(),
|
|
153
|
+
/**
|
|
154
|
+
* WHO marked it, when a person did — a name, snapshotted at the time.
|
|
155
|
+
*
|
|
156
|
+
* Null for `scan` and `auto`, which have no author. A snapshot rather than a
|
|
157
|
+
* live lookup because this is a record of what happened: a manager who later
|
|
158
|
+
* leaves, or changes their name, must not silently rewrite last month's
|
|
159
|
+
* attendance sheet.
|
|
160
|
+
*/
|
|
161
|
+
checkInBy: z.string().nullable(),
|
|
162
|
+
checkOutBy: z.string().nullable(),
|
|
129
163
|
});
|
|
130
164
|
export const attendanceQuerySchema = paginationSchema.extend({
|
|
131
165
|
userId: objectIdSchema.optional(),
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Workout sessions, check-in & attendance\n * ========================================================\n * A member checks in to start a live session, marks exercises done, and checks\n * out. Sessions roll up into attendance records the owner can review, and can\n * also be recorded directly by an owner (manual attendance marking).\n *\n * @module gymmonk-schema/session\n */\n\nimport { z } from 'zod';\nimport { checkInCodeSchema } from './check-in.js';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n} from './common.js';\nimport { checkInGeoInputSchema } from './location.js';\nimport { weekdaySchema } from './shared.js';\n\n// ─── Live session widget status ──────────────────────────────────────────────\n\nexport const sessionStatusSchema = z.enum(['idle', 'active', 'completed']);\nexport type SessionStatus = z.infer<typeof sessionStatusSchema>;\n\n// ─── The set stopwatch ───────────────────────────────────────────────────────\n\n/**\n * The member's own stopwatch, which is NOT the session clock.\n *\n * Home shows two running times and they measure different things. The session\n * clock is \"how long have you been in the gym\" — it runs from `checkInAt`,\n * cannot be paused, and is derived rather than stored. This one is \"how long\n * has this set taken\", and the member starts, pauses and resets it freely.\n *\n * Stored as ACCUMULATED + STARTED-AT rather than as a running total, because a\n * total would need the server to tick. Elapsed is\n * `accumulatedSec + (running ? now - startedAt : 0)`, so a paused stopwatch is\n * a plain number, a running one survives a refresh or a dead battery, and no\n * job has to write to the database once a second.\n *\n * `startedAt` is null exactly when `running` is false; the pair is a small\n * state machine, and the backend is the only writer.\n */\nexport const sessionStopwatchSchema = z.object({\n running: z.boolean(),\n /** Seconds banked by previous runs, excluding any run in progress. */\n accumulatedSec: z.number().int().nonnegative(),\n /** When the current run began. Null while paused. */\n startedAt: isoDateTimeSchema.nullable(),\n});\nexport type SessionStopwatch = z.infer<typeof sessionStopwatchSchema>;\n\n/** A stopwatch that has never been started. */\nexport const IDLE_STOPWATCH: SessionStopwatch = {\n running: false,\n accumulatedSec: 0,\n startedAt: null,\n};\n\n/**\n * Resolve a stopwatch to whole seconds elapsed.\n *\n * Shared rather than reimplemented on each side: the backend needs it to bank\n * time on pause, and the client needs it to render every tick. Two copies of\n * this arithmetic would drift the moment one of them forgot the running run.\n */\nexport function stopwatchElapsedSec(stopwatch: SessionStopwatch, now: Date = new Date()): number {\n if (!stopwatch.running || !stopwatch.startedAt) return stopwatch.accumulatedSec;\n const started = Date.parse(stopwatch.startedAt);\n if (Number.isNaN(started)) return stopwatch.accumulatedSec;\n // Floored at the banked value: a client clock behind the server's would\n // otherwise make the number run backwards.\n return stopwatch.accumulatedSec + Math.max(0, Math.floor((now.getTime() - started) / 1000));\n}\n\n// ─── Session entity ──────────────────────────────────────────────────────────\n\nexport const workoutSessionSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema,\n centerId: objectIdSchema,\n status: sessionStatusSchema,\n checkInAt: isoDateTimeSchema,\n checkOutAt: isoDateTimeSchema.nullable(),\n /** The plan weekday being trained, if the member is on a plan. */\n planWeekday: weekdaySchema.nullable(),\n completedExerciseIds: z.array(objectIdSchema),\n totalExercises: z.number().int().nonnegative(),\n stopwatch: sessionStopwatchSchema,\n});\nexport type WorkoutSession = z.infer<typeof workoutSessionSchema>;\n\n// ─── Session mutations ───────────────────────────────────────────────────────\n\n/**\n * Check in by scanning the gym's QR poster.\n *\n * `code` is REQUIRED and is the whole point: the center is resolved FROM the\n * scanned code, never from a client-supplied `centerId`. An earlier version of\n * this body took an optional `centerId`, which meant any authenticated member\n * could mark themselves present from their sofa. Presence has to be proven by\n * being close enough to the poster to photograph it.\n *\n * Staff marking someone present by hand is a different, authorised path —\n * `markAttendanceBodySchema` below.\n */\nexport const checkInBodySchema = z\n .object({\n code: checkInCodeSchema,\n })\n // Where the scanning device thought it was. All three optional — see\n // `checkInGeoInputSchema`. The server measures this against the gym's pin and\n // treats a missing fix as unverifiable rather than as a failure.\n .extend(checkInGeoInputSchema.shape);\nexport type CheckInBody = z.infer<typeof checkInBodySchema>;\n\n/**\n * Check out — the same poster, scanned again. Verified against the session's\n * OWN center, so a member cannot close a session by scanning a different gym.\n */\nexport const checkOutBodySchema = z.object({\n code: checkInCodeSchema,\n});\nexport type CheckOutBody = z.infer<typeof checkOutBodySchema>;\n\nexport const markExerciseDoneBodySchema = z.object({\n exerciseId: objectIdSchema,\n done: z.boolean(),\n});\nexport type MarkExerciseDoneBody = z.infer<typeof markExerciseDoneBodySchema>;\n\n/**\n * Drive the set stopwatch.\n *\n * An ACTION, not a state patch. The client says what the member pressed and the\n * server computes the resulting `{ running, accumulatedSec, startedAt }` — which\n * keeps the banking arithmetic in one place and means a client cannot post an\n * arbitrary elapsed time to inflate a workout.\n */\nexport const stopwatchActionBodySchema = z.object({\n action: z.enum(['start', 'pause', 'reset']),\n});\nexport type StopwatchActionBody = z.infer<typeof stopwatchActionBodySchema>;\n\n// ─── Attendance ──────────────────────────────────────────────────────────────\n\nexport const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);\nexport type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;\n\nexport const attendanceEntrySchema = z.object({\n date: isoDateSchema,\n checkInAt: isoDateTimeSchema.nullable(),\n checkOutAt: isoDateTimeSchema.nullable(),\n status: attendanceStatusSchema,\n});\nexport type AttendanceEntry = z.infer<typeof attendanceEntrySchema>;\n\nexport const attendanceQuerySchema = paginationSchema.extend({\n userId: objectIdSchema.optional(),\n from: isoDateSchema.optional(),\n to: isoDateSchema.optional(),\n});\nexport type AttendanceQuery = z.infer<typeof attendanceQuerySchema>;\n\n/** Owner marking attendance for a member or staff member manually. */\nexport const markAttendanceBodySchema = z.object({\n userId: objectIdSchema,\n date: isoDateSchema.optional(),\n status: attendanceStatusSchema.default('present'),\n checkInAt: isoDateTimeSchema.optional(),\n checkOutAt: isoDateTimeSchema.optional(),\n note: optionalTrimmedString(160),\n});\nexport type MarkAttendanceBody = z.infer<typeof markAttendanceBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Workout sessions, check-in & attendance\n * ========================================================\n * A member checks in to start a live session, marks exercises done, and checks\n * out. Sessions roll up into attendance records the owner can review, and can\n * also be recorded directly by an owner (manual attendance marking).\n *\n * @module gymmonk-schema/session\n */\n\nimport { z } from 'zod';\nimport { checkInCodeSchema } from './check-in.js';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n} from './common.js';\nimport { checkInGeoInputSchema } from './location.js';\nimport { weekdaySchema } from './shared.js';\n\n// ─── Live session widget status ──────────────────────────────────────────────\n\nexport const sessionStatusSchema = z.enum(['idle', 'active', 'completed']);\nexport type SessionStatus = z.infer<typeof sessionStatusSchema>;\n\n// ─── The set stopwatch ───────────────────────────────────────────────────────\n\n/**\n * The member's own stopwatch, which is NOT the session clock.\n *\n * Home shows two running times and they measure different things. The session\n * clock is \"how long have you been in the gym\" — it runs from `checkInAt`,\n * cannot be paused, and is derived rather than stored. This one is \"how long\n * has this set taken\", and the member starts, pauses and resets it freely.\n *\n * Stored as ACCUMULATED + STARTED-AT rather than as a running total, because a\n * total would need the server to tick. Elapsed is\n * `accumulatedSec + (running ? now - startedAt : 0)`, so a paused stopwatch is\n * a plain number, a running one survives a refresh or a dead battery, and no\n * job has to write to the database once a second.\n *\n * `startedAt` is null exactly when `running` is false; the pair is a small\n * state machine, and the backend is the only writer.\n */\nexport const sessionStopwatchSchema = z.object({\n running: z.boolean(),\n /** Seconds banked by previous runs, excluding any run in progress. */\n accumulatedSec: z.number().int().nonnegative(),\n /** When the current run began. Null while paused. */\n startedAt: isoDateTimeSchema.nullable(),\n});\nexport type SessionStopwatch = z.infer<typeof sessionStopwatchSchema>;\n\n/** A stopwatch that has never been started. */\nexport const IDLE_STOPWATCH: SessionStopwatch = {\n running: false,\n accumulatedSec: 0,\n startedAt: null,\n};\n\n/**\n * Resolve a stopwatch to whole seconds elapsed.\n *\n * Shared rather than reimplemented on each side: the backend needs it to bank\n * time on pause, and the client needs it to render every tick. Two copies of\n * this arithmetic would drift the moment one of them forgot the running run.\n */\nexport function stopwatchElapsedSec(stopwatch: SessionStopwatch, now: Date = new Date()): number {\n if (!stopwatch.running || !stopwatch.startedAt) return stopwatch.accumulatedSec;\n const started = Date.parse(stopwatch.startedAt);\n if (Number.isNaN(started)) return stopwatch.accumulatedSec;\n // Floored at the banked value: a client clock behind the server's would\n // otherwise make the number run backwards.\n return stopwatch.accumulatedSec + Math.max(0, Math.floor((now.getTime() - started) / 1000));\n}\n\n// ─── Session entity ──────────────────────────────────────────────────────────\n\nexport const workoutSessionSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema,\n centerId: objectIdSchema,\n status: sessionStatusSchema,\n checkInAt: isoDateTimeSchema,\n checkOutAt: isoDateTimeSchema.nullable(),\n /** The plan weekday being trained, if the member is on a plan. */\n planWeekday: weekdaySchema.nullable(),\n completedExerciseIds: z.array(objectIdSchema),\n totalExercises: z.number().int().nonnegative(),\n stopwatch: sessionStopwatchSchema,\n});\nexport type WorkoutSession = z.infer<typeof workoutSessionSchema>;\n\n// ─── Session mutations ───────────────────────────────────────────────────────\n\n/**\n * Check in by scanning the gym's QR poster.\n *\n * `code` is REQUIRED and is the whole point: the center is resolved FROM the\n * scanned code, never from a client-supplied `centerId`. An earlier version of\n * this body took an optional `centerId`, which meant any authenticated member\n * could mark themselves present from their sofa. Presence has to be proven by\n * being close enough to the poster to photograph it.\n *\n * Staff marking someone present by hand is a different, authorised path —\n * `markAttendanceBodySchema` below.\n */\nexport const checkInBodySchema = z\n .object({\n code: checkInCodeSchema,\n })\n // Where the scanning device thought it was. All three optional — see\n // `checkInGeoInputSchema`. The server measures this against the gym's pin and\n // treats a missing fix as unverifiable rather than as a failure.\n .extend(checkInGeoInputSchema.shape);\nexport type CheckInBody = z.infer<typeof checkInBodySchema>;\n\n/**\n * Check out — the same poster, scanned again. Verified against the session's\n * OWN center, so a member cannot close a session by scanning a different gym.\n */\nexport const checkOutBodySchema = z.object({\n code: checkInCodeSchema,\n});\nexport type CheckOutBody = z.infer<typeof checkOutBodySchema>;\n\nexport const markExerciseDoneBodySchema = z.object({\n exerciseId: objectIdSchema,\n done: z.boolean(),\n});\nexport type MarkExerciseDoneBody = z.infer<typeof markExerciseDoneBodySchema>;\n\n/**\n * Drive the set stopwatch.\n *\n * An ACTION, not a state patch. The client says what the member pressed and the\n * server computes the resulting `{ running, accumulatedSec, startedAt }` — which\n * keeps the banking arithmetic in one place and means a client cannot post an\n * arbitrary elapsed time to inflate a workout.\n */\nexport const stopwatchActionBodySchema = z.object({\n action: z.enum(['start', 'pause', 'reset']),\n});\nexport type StopwatchActionBody = z.infer<typeof stopwatchActionBodySchema>;\n\n// ─── Attendance ──────────────────────────────────────────────────────────────\n\nexport const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);\nexport type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;\n\n/**\n * How an attendance mark came to be recorded.\n *\n * `scan` — the member scanned the gym's QR poster themselves.\n * `manual` — a person put it there: an owner, manager or front desk.\n * `auto` — nobody did. The member never scanned out and the session hit its\n * cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).\n *\n * Recorded per SIDE, because the two halves of a visit routinely differ: the\n * common case is a member who scans in and is closed out automatically. An\n * attendance report that cannot tell a scanned departure from an assumed one is\n * reporting a guess as a measurement.\n */\nexport const attendanceSourceSchema = z.enum(['scan', 'manual', 'auto']);\nexport type AttendanceSource = z.infer<typeof attendanceSourceSchema>;\n\n/**\n * How long a session may stay open before it is closed for the member.\n *\n * Shared with the client so the member's screen can say when their session will\n * end, rather than having the number live only in the server that enforces it.\n */\nexport const AUTO_CHECK_OUT_HOURS = 3;\n\nexport const attendanceEntrySchema = z.object({\n date: isoDateSchema,\n checkInAt: isoDateTimeSchema.nullable(),\n checkOutAt: isoDateTimeSchema.nullable(),\n status: attendanceStatusSchema,\n /** How each half was recorded. Null where that half has not happened. */\n checkInSource: attendanceSourceSchema.nullable(),\n checkOutSource: attendanceSourceSchema.nullable(),\n /**\n * WHO marked it, when a person did — a name, snapshotted at the time.\n *\n * Null for `scan` and `auto`, which have no author. A snapshot rather than a\n * live lookup because this is a record of what happened: a manager who later\n * leaves, or changes their name, must not silently rewrite last month's\n * attendance sheet.\n */\n checkInBy: z.string().nullable(),\n checkOutBy: z.string().nullable(),\n});\nexport type AttendanceEntry = z.infer<typeof attendanceEntrySchema>;\n\nexport const attendanceQuerySchema = paginationSchema.extend({\n userId: objectIdSchema.optional(),\n from: isoDateSchema.optional(),\n to: isoDateSchema.optional(),\n});\nexport type AttendanceQuery = z.infer<typeof attendanceQuerySchema>;\n\n/** Owner marking attendance for a member or staff member manually. */\nexport const markAttendanceBodySchema = z.object({\n userId: objectIdSchema,\n date: isoDateSchema.optional(),\n status: attendanceStatusSchema.default('present'),\n checkInAt: isoDateTimeSchema.optional(),\n checkOutAt: isoDateTimeSchema.optional(),\n note: optionalTrimmedString(160),\n});\nexport type MarkAttendanceBody = z.infer<typeof markAttendanceBodySchema>;\n"]}
|
package/dist/staff.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { z } from 'zod';
|
|
11
11
|
import { isoDateSchema, isoDateTimeSchema, objectIdSchema, optionalTrimmedString, phoneSchema, priceInrSchema, } from './common.js';
|
|
12
12
|
import { memberInviteSchema } from './member.js';
|
|
13
|
-
import { emergencyContactSchema } from './member-profile.js';
|
|
13
|
+
import { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';
|
|
14
14
|
import { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';
|
|
15
15
|
// ─── Role & status ───────────────────────────────────────────────────────────
|
|
16
16
|
export const staffRoleSchema = z.enum(['trainer', 'manager', 'cleaning', 'reception']);
|
|
@@ -106,7 +106,7 @@ export const createStaffBodySchema = z.object({
|
|
|
106
106
|
dob: isoDateSchema.optional(),
|
|
107
107
|
bloodGroup: bloodGroupSchema.optional(),
|
|
108
108
|
address: addressSchema.optional(),
|
|
109
|
-
emergencyContacts: z.array(emergencyContactSchema).max(
|
|
109
|
+
emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),
|
|
110
110
|
idProofUrl: optionalTrimmedString(2000),
|
|
111
111
|
avatarUrl: optionalTrimmedString(2000),
|
|
112
112
|
});
|
package/dist/staff.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"staff.js","sourceRoot":"","sources":["../src/staff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Staff (trainers, managers, support)\n * ====================================================\n * A center's employees. Like members, adding a staff member provisions a real\n * central-auth account (returns login email + temp password). Trainers can have\n * members assigned to them.\n *\n * @module gymmonk-schema/staff\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n phoneSchema,\n priceInrSchema,\n} from './common.js';\nimport { memberInviteSchema } from './member.js';\nimport { emergencyContactSchema } from './member-profile.js';\nimport { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Role & status ───────────────────────────────────────────────────────────\n\nexport const staffRoleSchema = z.enum(['trainer', 'manager', 'cleaning', 'reception']);\nexport type StaffRole = z.infer<typeof staffRoleSchema>;\n\nexport const staffStatusSchema = z.enum(['active', 'inactive']);\nexport type StaffStatus = z.infer<typeof staffStatusSchema>;\n\n// ─── List projection ─────────────────────────────────────────────────────────\n\nexport const staffListItemSchema = z.object({\n /** The staff record id — always present, and what every `/staff/:id` route keys on. */\n id: objectIdSchema,\n /** Null until the staff member claims their invite (same model as members). */\n userId: objectIdSchema.nullable(),\n staffCode: z.string(),\n name: z.string(),\n role: staffRoleSchema,\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n status: staffStatusSchema,\n onDuty: z.boolean(),\n /** Contact, so the roster card can call / message without opening the record. */\n phone: z.string().nullable(),\n email: z.email().nullable(),\n /** `YYYY-MM-DD`. The card shows an age beside the name. */\n dob: isoDateSchema.nullable(),\n /** When the gym took them on — not when they claimed their invite. */\n joinedAt: isoDateTimeSchema,\n /** Last recorded check-in, or null. Attendance is keyed on the claimed user. */\n lastCheckInAt: isoDateTimeSchema.nullable(),\n /** Whether their latest payout is settled — the card's salary pill. */\n balancePaid: z.boolean(),\n});\nexport type StaffListItem = z.infer<typeof staffListItemSchema>;\n\nexport const staffListSummarySchema = z.object({\n total: z.number().int().nonnegative(),\n active: z.number().int().nonnegative(),\n onDuty: z.number().int().nonnegative(),\n});\nexport type StaffListSummary = z.infer<typeof staffListSummarySchema>;\n\n// ─── Detail projection ───────────────────────────────────────────────────────\n\n/** A member assigned to a trainer, shown on the trainer detail. */\nexport const assignedMemberRefSchema = z.object({\n /** The member RECORD id — what `/members/:id` keys on, so the row can link. */\n memberProfileId: objectIdSchema,\n userId: objectIdSchema.nullable(),\n name: z.string(),\n avatarUrl: z.string().nullable(),\n note: optionalTrimmedString(160),\n});\nexport type AssignedMemberRef = z.infer<typeof assignedMemberRefSchema>;\n\nexport const staffDetailSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema.nullable(),\n staffCode: z.string(),\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n role: staffRoleSchema,\n status: staffStatusSchema,\n onDuty: z.boolean(),\n verified: z.boolean(),\n gender: genderSchema.nullable(),\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n address: addressSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n joinedAt: isoDateTimeSchema,\n /** Whether the latest salary/dues are settled. */\n balancePaid: z.boolean(),\n nextPaymentDate: isoDateSchema.nullable(),\n /** Populated for trainers only. */\n assignedMembers: z.array(assignedMemberRefSchema),\n});\nexport type StaffDetail = z.infer<typeof staffDetailSchema>;\n\n// ─── Payroll ─────────────────────────────────────────────────────────────────\n\nexport const staffPaymentStatusSchema = z.enum(['success', 'failed', 'pending']);\nexport type StaffPaymentStatus = z.infer<typeof staffPaymentStatusSchema>;\n\n/** A salary / payout entry against a staff member. */\nexport const staffPaymentSchema = z.object({\n id: objectIdSchema,\n staffUserId: objectIdSchema,\n amountInr: priceInrSchema,\n status: staffPaymentStatusSchema,\n paidAt: isoDateTimeSchema,\n note: z.string().nullable(),\n});\nexport type StaffPayment = z.infer<typeof staffPaymentSchema>;\n\nexport const recordStaffPaymentBodySchema = z.object({\n amountInr: priceInrSchema,\n status: staffPaymentStatusSchema.default('success'),\n note: optionalTrimmedString(240),\n});\nexport type RecordStaffPaymentBody = z.infer<typeof recordStaffPaymentBodySchema>;\n\n// ─── Add staff (provisioning) ────────────────────────────────────────────────\n\nexport const createStaffBodySchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n email: z.email('A valid email is required'),\n phone: phoneSchema,\n role: staffRoleSchema,\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n bloodGroup: bloodGroupSchema.optional(),\n address: addressSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(5).default([]),\n idProofUrl: optionalTrimmedString(2000),\n avatarUrl: optionalTrimmedString(2000),\n});\nexport type CreateStaffBody = z.infer<typeof createStaffBodySchema>;\n\n/**\n * Adding staff creates a gym record + an invite, exactly like members — the gym\n * never mints central-auth credentials. The gym ROLE (trainer / manager / …) is\n * product data enforced by gymmonk-backend, never a platform-wide auth role.\n */\nexport const staffProvisionResultSchema = z.object({\n staffId: objectIdSchema,\n code: z.string(),\n invite: memberInviteSchema,\n});\nexport type StaffProvisionResult = z.infer<typeof staffProvisionResultSchema>;\n\n// ─── Mutations ───────────────────────────────────────────────────────────────\n\nexport const setStaffDutyBodySchema = z.object({ onDuty: z.boolean() });\nexport type SetStaffDutyBody = z.infer<typeof setStaffDutyBodySchema>;\n\nexport const setStaffStatusBodySchema = z.object({ active: z.boolean() });\nexport type SetStaffStatusBody = z.infer<typeof setStaffStatusBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"staff.js","sourceRoot":"","sources":["../src/staff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Staff (trainers, managers, support)\n * ====================================================\n * A center's employees. Like members, adding a staff member provisions a real\n * central-auth account (returns login email + temp password). Trainers can have\n * members assigned to them.\n *\n * @module gymmonk-schema/staff\n */\n\nimport { z } from 'zod';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n phoneSchema,\n priceInrSchema,\n} from './common.js';\nimport { memberInviteSchema } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport { addressSchema, bloodGroupSchema, genderSchema } from './shared.js';\n\n// ─── Role & status ───────────────────────────────────────────────────────────\n\nexport const staffRoleSchema = z.enum(['trainer', 'manager', 'cleaning', 'reception']);\nexport type StaffRole = z.infer<typeof staffRoleSchema>;\n\nexport const staffStatusSchema = z.enum(['active', 'inactive']);\nexport type StaffStatus = z.infer<typeof staffStatusSchema>;\n\n// ─── List projection ─────────────────────────────────────────────────────────\n\nexport const staffListItemSchema = z.object({\n /** The staff record id — always present, and what every `/staff/:id` route keys on. */\n id: objectIdSchema,\n /** Null until the staff member claims their invite (same model as members). */\n userId: objectIdSchema.nullable(),\n staffCode: z.string(),\n name: z.string(),\n role: staffRoleSchema,\n avatarUrl: z.string().nullable(),\n verified: z.boolean(),\n status: staffStatusSchema,\n onDuty: z.boolean(),\n /** Contact, so the roster card can call / message without opening the record. */\n phone: z.string().nullable(),\n email: z.email().nullable(),\n /** `YYYY-MM-DD`. The card shows an age beside the name. */\n dob: isoDateSchema.nullable(),\n /** When the gym took them on — not when they claimed their invite. */\n joinedAt: isoDateTimeSchema,\n /** Last recorded check-in, or null. Attendance is keyed on the claimed user. */\n lastCheckInAt: isoDateTimeSchema.nullable(),\n /** Whether their latest payout is settled — the card's salary pill. */\n balancePaid: z.boolean(),\n});\nexport type StaffListItem = z.infer<typeof staffListItemSchema>;\n\nexport const staffListSummarySchema = z.object({\n total: z.number().int().nonnegative(),\n active: z.number().int().nonnegative(),\n onDuty: z.number().int().nonnegative(),\n});\nexport type StaffListSummary = z.infer<typeof staffListSummarySchema>;\n\n// ─── Detail projection ───────────────────────────────────────────────────────\n\n/** A member assigned to a trainer, shown on the trainer detail. */\nexport const assignedMemberRefSchema = z.object({\n /** The member RECORD id — what `/members/:id` keys on, so the row can link. */\n memberProfileId: objectIdSchema,\n userId: objectIdSchema.nullable(),\n name: z.string(),\n avatarUrl: z.string().nullable(),\n note: optionalTrimmedString(160),\n});\nexport type AssignedMemberRef = z.infer<typeof assignedMemberRefSchema>;\n\nexport const staffDetailSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema.nullable(),\n staffCode: z.string(),\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n role: staffRoleSchema,\n status: staffStatusSchema,\n onDuty: z.boolean(),\n verified: z.boolean(),\n gender: genderSchema.nullable(),\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n address: addressSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\n joinedAt: isoDateTimeSchema,\n /** Whether the latest salary/dues are settled. */\n balancePaid: z.boolean(),\n nextPaymentDate: isoDateSchema.nullable(),\n /** Populated for trainers only. */\n assignedMembers: z.array(assignedMemberRefSchema),\n});\nexport type StaffDetail = z.infer<typeof staffDetailSchema>;\n\n// ─── Payroll ─────────────────────────────────────────────────────────────────\n\nexport const staffPaymentStatusSchema = z.enum(['success', 'failed', 'pending']);\nexport type StaffPaymentStatus = z.infer<typeof staffPaymentStatusSchema>;\n\n/** A salary / payout entry against a staff member. */\nexport const staffPaymentSchema = z.object({\n id: objectIdSchema,\n staffUserId: objectIdSchema,\n amountInr: priceInrSchema,\n status: staffPaymentStatusSchema,\n paidAt: isoDateTimeSchema,\n note: z.string().nullable(),\n});\nexport type StaffPayment = z.infer<typeof staffPaymentSchema>;\n\nexport const recordStaffPaymentBodySchema = z.object({\n amountInr: priceInrSchema,\n status: staffPaymentStatusSchema.default('success'),\n note: optionalTrimmedString(240),\n});\nexport type RecordStaffPaymentBody = z.infer<typeof recordStaffPaymentBodySchema>;\n\n// ─── Add staff (provisioning) ────────────────────────────────────────────────\n\nexport const createStaffBodySchema = z.object({\n firstName: z.string().trim().min(1, 'First name is required').max(80),\n lastName: optionalTrimmedString(80),\n email: z.email('A valid email is required'),\n phone: phoneSchema,\n role: staffRoleSchema,\n gender: genderSchema,\n dob: isoDateSchema.optional(),\n bloodGroup: bloodGroupSchema.optional(),\n address: addressSchema.optional(),\n emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\n idProofUrl: optionalTrimmedString(2000),\n avatarUrl: optionalTrimmedString(2000),\n});\nexport type CreateStaffBody = z.infer<typeof createStaffBodySchema>;\n\n/**\n * Adding staff creates a gym record + an invite, exactly like members — the gym\n * never mints central-auth credentials. The gym ROLE (trainer / manager / …) is\n * product data enforced by gymmonk-backend, never a platform-wide auth role.\n */\nexport const staffProvisionResultSchema = z.object({\n staffId: objectIdSchema,\n code: z.string(),\n invite: memberInviteSchema,\n});\nexport type StaffProvisionResult = z.infer<typeof staffProvisionResultSchema>;\n\n// ─── Mutations ───────────────────────────────────────────────────────────────\n\nexport const setStaffDutyBodySchema = z.object({ onDuty: z.boolean() });\nexport type SetStaffDutyBody = z.infer<typeof setStaffDutyBodySchema>;\n\nexport const setStaffStatusBodySchema = z.object({ active: z.boolean() });\nexport type SetStaffStatusBody = z.infer<typeof setStaffStatusBodySchema>;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gymmonk-schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS) — single source of truth (SSOT) consumed by gymmonk-backend and gymmonk-web-client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|