gymmonk-schema 0.45.0 → 0.46.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.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/member.d.ts +2 -0
- package/dist/member.d.ts.map +1 -1
- package/dist/member.js +13 -0
- package/dist/member.js.map +1 -1
- package/dist/membership-plan.d.ts +6 -6
- package/dist/membership-status.d.ts +255 -0
- package/dist/membership-status.d.ts.map +1 -0
- package/dist/membership-status.js +340 -0
- package/dist/membership-status.js.map +1 -0
- package/dist/membership.d.ts +1 -56
- package/dist/membership.d.ts.map +1 -1
- package/dist/membership.js +8 -75
- package/dist/membership.js.map +1 -1
- package/dist/onboarding-form.d.ts +0 -8
- package/dist/onboarding-form.d.ts.map +1 -1
- package/dist/onboarding-form.js +8 -28
- package/dist/onboarding-form.js.map +1 -1
- package/dist/onboarding.d.ts +4 -4
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +16 -2
- package/dist/onboarding.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-person-form.js","sourceRoot":"","sources":["../src/add-person-form.ts"],"names":[],"mappings":"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","sourcesContent":["/**\r\n * gymmonk-schema — \"Add member\" / \"Add staff\" FORM schemas\r\n * ========================================================\r\n * The client-side validation contract for the owner's desk-entry forms, and the\r\n * mappers that turn a filled form into the `createMember` / `createStaff` API\r\n * bodies.\r\n *\r\n * Why these live here rather than in the client, and why they are distinct from\r\n * `createMemberBodySchema`: the form's working shape is not the wire shape. It\r\n * holds ONE \"name\" field the API splits in two, `null` for a choice not yet\r\n * made, and `''` for an optional value left blank. Those are legitimate mid-edit\r\n * states that the API body must never see. Expressing the same rules over the\r\n * form shape here keeps\r\n * every message, bound and regex defined once — the client cannot drift from\r\n * what the server will accept, and the coercion from one shape to the other is\r\n * part of the contract instead of ad-hoc code in a component.\r\n *\r\n * Mirrors `onboarding-form.ts`, which does the same job for the wizards.\r\n *\r\n * @module gymmonk-schema/add-person-form\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport { ageInYears, isRealDate, PHONE_REGEX } from './common.js';\r\nimport { DEFAULT_TIMEZONE } from './location.js';\r\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\r\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\r\nimport type { AssignMembershipBody } from './membership.js';\r\nimport { bloodGroupSchema, genderSchema } from './shared.js';\r\nimport type { CreateStaffBody } from './staff.js';\r\nimport { staffRoleSchema } from './staff.js';\r\n\r\n// ─── Bounds ──────────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\r\n * because a gym also enrols juniors with a guardian present, and the date is\r\n * typed by staff rather than by the person themselves.\r\n */\r\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\r\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\r\n\r\n// ─── Date of birth, as three cells ───────────────────────────────────────────\r\n\r\n/**\r\n * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`\r\n * when nothing was entered.\r\n *\r\n * Desk entry does not require a date of birth — a gym adds people with\r\n * whatever it has to hand — so \"blank\" is a real answer and has to survive the\r\n * whole way to the body as an ABSENT field rather than an empty string the\r\n * API would reject.\r\n */\r\nexport function isoFromDobCells(day: string, month: string, year: string): string | undefined {\r\n if (!day || !month || !year) return undefined;\r\n return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;\r\n}\r\n\r\n/** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */\r\nexport function dobCellsFromIso(iso: string | null | undefined): {\r\n dobDay: string;\r\n dobMonth: string;\r\n dobYear: string;\r\n} {\r\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso ?? '');\r\n if (!match) return { dobDay: '', dobMonth: '', dobYear: '' };\r\n return { dobYear: match[1] as string, dobMonth: match[2] as string, dobDay: match[3] as string };\r\n}\r\n\r\n// ─── Field-level rules ───────────────────────────────────────────────────────\r\n\r\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\r\n inner.nullable().refine((v: unknown) => v !== null, { message });\r\n\r\n// ─── One emergency-contact row ───────────────────────────────────────────────\r\n\r\n/**\r\n * One emergency-contact row: WHO to call, on what number, and how they are\r\n * related. A row is either wholly blank (ignored) or names a person.\r\n *\r\n * ── Why `name` exists now ───────────────────────────────────────────────────\r\n * This form used to collect a relationship and a phone with no name at all,\r\n * and the mapper wrote the relationship INTO `name` because\r\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\r\n * name, so the two halves of the product disagreed about what the field held,\r\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\r\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\r\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\r\n * called Sister. Whoever has to ring that number in an emergency is left with\r\n * a word instead of a person.\r\n *\r\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\r\n * useful context, not something to block a desk entry over.\r\n */\r\nexport const emergencyContactRowSchema = z\r\n .object({\r\n name: z.string().trim().max(120),\r\n relationship: z.string().trim().max(60),\r\n phone: z.string().trim(),\r\n })\r\n .superRefine((v, ctx) => {\r\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\r\n if (blank) return;\r\n if (v.name === '') {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['name'],\r\n message: \"Add the contact's name\",\r\n });\r\n }\r\n if (!PHONE_REGEX.test(v.phone)) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['phone'],\r\n message: 'Enter a valid 10-digit mobile number',\r\n });\r\n }\r\n });\r\n\r\nexport interface EmergencyContactRow {\r\n name: string;\r\n relationship: string;\r\n phone: string;\r\n}\r\n\r\n// ─── Shared base ─────────────────────────────────────────────────────────────\r\n\r\nconst addPersonBaseShape = {\r\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\r\n name: z.string().trim().min(1, 'Name is required').max(160),\r\n email: z.email('Enter a valid email'),\r\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\r\n /**\r\n * Three cells, not a native date input.\r\n *\r\n * `<input type=\"date\">` renders in the BROWSER'S locale, so the same field\r\n * read \"mm/dd/yyyy\" at an Indian gym's front desk — and a member's own\r\n * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY\r\n * cells. Two surfaces collecting one fact in two formats is how a birthday\r\n * gets typed the wrong way round.\r\n *\r\n * All three or none: desk entry does not require a date of birth, so blank\r\n * is a real answer, but half a date is not.\r\n */\r\n dobDay: z.string(),\r\n dobMonth: z.string(),\r\n dobYear: z.string(),\r\n gender: requiredChoice(genderSchema, 'Select a gender'),\r\n\r\n /** Blank when not recorded. */\r\n bloodGroup: bloodGroupSchema.or(z.literal('')),\r\n avatarUrl: z.string(),\r\n\r\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\r\n};\r\n\r\n/**\r\n * Date of birth is optional, but a typed one has to be real and plausible —\r\n * a mis-keyed year is the single most common desk-entry slip.\r\n */\r\nfunction refineDob(\r\n v: { dobDay: string; dobMonth: string; dobYear: string },\r\n ctx: z.RefinementCtx,\r\n): void {\r\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\r\n // Nothing entered is fine — the desk records what it has.\r\n if (!touched) return;\r\n\r\n // Every message lands on `dobDay`, the first cell, because the three are one\r\n // control on screen: an error parked on `dobYear` would sit under a field\r\n // the form renders as a single row and reads back from `dobDay`.\r\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });\r\n return;\r\n }\r\n\r\n const day = Number(v.dobDay);\r\n const month = Number(v.dobMonth);\r\n const year = Number(v.dobYear);\r\n\r\n if (!isRealDate(year, month, day)) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\r\n return;\r\n }\r\n\r\n const age = ageInYears(year, month, day);\r\n if (age < 0) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['dobDay'],\r\n message: 'Date of birth cannot be in the future',\r\n });\r\n } else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {\r\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\r\n }\r\n}\r\n\r\n// ─── Member form ─────────────────────────────────────────────────────────────\r\n\r\nexport const addMemberFormSchema = z\r\n .object({\r\n ...addPersonBaseShape,\r\n /**\r\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\r\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\r\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\r\n */\r\n trainerId: z.string(),\r\n })\r\n .superRefine((v, ctx) => {\r\n refineDob(v, ctx);\r\n });\r\n\r\n/**\r\n * Adding a member asks for the person, not their paperwork.\r\n *\r\n * No address, no Aadhaar and no ID proof — on this form or on any other. A\r\n * front desk does not have them when somebody walks in, the member's own\r\n * screens never asked for them, and nothing in the product ever read one back.\r\n * Every surface that collected them was filling a drawer nobody opens.\r\n */\r\nexport type AddMemberFormValues = {\r\n name: string;\r\n email: string;\r\n phone: string;\r\n dobDay: string;\r\n dobMonth: string;\r\n dobYear: string;\r\n gender: z.infer<typeof genderSchema> | null;\r\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\r\n avatarUrl: string;\r\n contacts: EmergencyContactRow[];\r\n /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */\r\n trainerId: string;\r\n};\r\n\r\n// ─── Staff form ──────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Staff are asked exactly what a member is asked, plus the job they do.\r\n *\r\n * The home address and the ID upload both went for the same reason: nothing in\r\n * the product ever showed either one back. A gym that needs an employee's\r\n * address for a contract has somewhere else to keep it; what it had here was a\r\n * field collected once and never looked at again, which is the worst thing to\r\n * ask anyone for.\r\n */\r\nexport const addStaffFormSchema = z\r\n .object({\r\n ...addPersonBaseShape,\r\n role: requiredChoice(staffRoleSchema, 'Select a staff role'),\r\n })\r\n .superRefine((v, ctx) => {\r\n refineDob(v, ctx);\r\n });\r\n\r\nexport type AddStaffFormValues = Omit<AddMemberFormValues, 'trainerId'> & {\r\n role: z.infer<typeof staffRoleSchema> | null;\r\n};\r\n\r\n// ─── Empty states ────────────────────────────────────────────────────────────\r\n\r\nconst EMPTY_BASE = {\r\n name: '',\r\n email: '',\r\n phone: '',\r\n dobDay: '',\r\n dobMonth: '',\r\n dobYear: '',\r\n gender: null,\r\n bloodGroup: '' as const,\r\n avatarUrl: '',\r\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\r\n\r\n/** One blank row so the section renders with a contact to fill. */\r\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\r\n name: '',\r\n relationship: '',\r\n phone: '',\r\n};\r\n\r\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\r\n ...EMPTY_BASE,\r\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\r\n trainerId: '',\r\n};\r\n\r\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\r\n ...EMPTY_BASE,\r\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\r\n role: null,\r\n};\r\n\r\n// ─── Form → API body ─────────────────────────────────────────────────────────\r\n\r\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\r\nfunction splitName(value: string): { firstName: string; lastName?: string } {\r\n const parts = value.trim().split(/\\s+/).filter(Boolean);\r\n const lastName = parts.slice(1).join(' ');\r\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\r\n}\r\n\r\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\r\nfunction orUndefined(value: string): string | undefined {\r\n const trimmed = value.trim();\r\n return trimmed === '' ? undefined : trimmed;\r\n}\r\n\r\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\r\nfunction asPhone(value: string): string {\r\n return value.replace(/\\D/g, '').slice(-10);\r\n}\r\n\r\n/**\r\n * The fields EVERY desk form sends.\r\n *\r\n * All three now ask exactly the same things about the PERSON; what differs is\r\n * what the record is FOR, so each mapper adds its own role / trainer / plan on\r\n * top. Address and ID proof used to be the exception here, asked by two of the\r\n * three; they are gone from all of them.\r\n */\r\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\r\n // A row with nothing in it is not a contact. Anything else has already been\r\n // through `emergencyContactRowSchema`, which requires a name and a valid\r\n // phone the moment a row is touched.\r\n const emergencyContacts = v.contacts\r\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\r\n .map((c) =>\r\n emergencyContactSchema.parse({\r\n name: c.name.trim(),\r\n phone: asPhone(c.phone),\r\n relationship: orUndefined(c.relationship),\r\n }),\r\n )\r\n .slice(0, MAX_EMERGENCY_CONTACTS);\r\n\r\n return {\r\n ...splitName(v.name),\r\n email: v.email.trim(),\r\n phone: asPhone(v.phone),\r\n // The form schema guarantees a choice was made before we get here.\r\n gender: v.gender ?? 'other',\r\n // Absent rather than empty when the desk did not record one — see\r\n // `isoFromDobCells`.\r\n dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),\r\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\r\n emergencyContacts,\r\n avatarUrl: orUndefined(v.avatarUrl),\r\n };\r\n}\r\n\r\n/**\r\n * Validated member form → `POST /members` body. `initialPlan` comes from the\r\n * membership step, which the form itself knows nothing about.\r\n */\r\nexport function toCreateMemberBody(\r\n values: AddMemberFormValues,\r\n initialPlan?: AssignMembershipBody,\r\n): CreateMemberBody {\r\n return {\r\n ...sharedBody(values),\r\n assignedTrainerId: orUndefined(values.trainerId),\r\n ...(initialPlan ? { initialPlan } : {}),\r\n };\r\n}\r\n\r\n/** Validated staff form → `POST /staff` body. */\r\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\r\n return {\r\n ...sharedBody(values),\r\n // The form schema guarantees a role was chosen.\r\n role: values.role ?? 'trainer',\r\n };\r\n}\r\n\r\n/**\r\n * The joining date must be a real day, and must already have happened.\r\n *\r\n * Validated here as well as on the server because this one is not a formatting\r\n * preference: the date anchors the member's whole billing cycle, so a typo\r\n * that slips through bills them on the wrong day of every month until somebody\r\n * notices. The server holds the same rule — this is the copy that can say so\r\n * before the owner presses save.\r\n */\r\nfunction refineJoiningDate(v: { joiningDate: string }, ctx: z.RefinementCtx): void {\r\n const raw = v.joiningDate.trim();\r\n // Blank is \"leave it as it is\", which is always allowed.\r\n if (!raw) return;\r\n\r\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw);\r\n if (!parts) {\r\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'Enter a valid date' });\r\n return;\r\n }\r\n\r\n const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];\r\n if (!isRealDate(year, month, day)) {\r\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'That date does not exist' });\r\n return;\r\n }\r\n\r\n /**\r\n * \"Today\" means today AT THE GYM, and the comparison is between strings.\r\n *\r\n * `en-CA` is the one common locale whose short format is already\r\n * `YYYY-MM-DD`, so the zone conversion and the formatting happen in one step\r\n * and the result sorts lexicographically — an exact calendar-day comparison\r\n * with no instant arithmetic in it. This mirrors `gymDayOf` on the server,\r\n * deliberately: a UTC day here would be one behind IST between midnight and\r\n * 5:30am, and would tell an owner opening the app early that TODAY is in the\r\n * future.\r\n */\r\n const today = new Intl.DateTimeFormat('en-CA', { timeZone: DEFAULT_TIMEZONE }).format(new Date());\r\n if (raw > today) {\r\n ctx.addIssue({\r\n code: 'custom',\r\n path: ['joiningDate'],\r\n message: 'A joining date cannot be in the future',\r\n });\r\n }\r\n}\r\n\r\n// ─── Edit member ─────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Editing a member asks for exactly what adding one asks for.\r\n *\r\n * It stays a separate schema and a separate mapper because the two differ in\r\n * what \"blank\" means. On the add form a blank optional is simply not provided;\r\n * on the edit form it is an instruction to CLEAR a value that is already\r\n * stored, so `toUpdateMemberBody` sends `null` where `toCreateMemberBody`\r\n * omits the field.\r\n */\r\nexport const editMemberFormSchema = z\r\n .object({\r\n ...addPersonBaseShape,\r\n trainerId: z.string(),\r\n /**\r\n * When the GYM says this person started training there, `YYYY-MM-DD`.\r\n *\r\n * Asked only on EDIT. At the desk the answer is always \"today\" and one\r\n * more field in a queue is one more thing to get wrong; the correction is\r\n * something the owner makes later, once they realise the app's record and\r\n * the gym's disagree.\r\n *\r\n * Blank means \"leave it alone\", never \"clear it\" — unlike the other\r\n * optional fields on this form. Every enrolment has a joining date because\r\n * the billing cycle is measured from one, so there is no empty state to\r\n * write back.\r\n */\r\n joiningDate: z.string(),\r\n })\r\n .superRefine((v, ctx) => {\r\n refineDob(v, ctx);\r\n refineJoiningDate(v, ctx);\r\n });\r\n\r\nexport type EditMemberFormValues = AddMemberFormValues & {\r\n joiningDate: string;\r\n};\r\n\r\n/**\r\n * Validated edit form → `PATCH /members/:id` body.\r\n *\r\n * Sends every field, including the cleared ones as `null`, because the owner\r\n * emptying a field is an edit like any other. The identity fields go along too;\r\n * the server drops them once the member has claimed their account.\r\n */\r\nexport function toUpdateMemberBody(values: EditMemberFormValues): UpdateMemberBody {\r\n const shared = sharedBody(values);\r\n return {\r\n ...shared,\r\n dob: shared.dob ?? null,\r\n bloodGroup: shared.bloodGroup ?? null,\r\n avatarUrl: values.avatarUrl.trim(),\r\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\r\n // OMITTED when blank rather than sent as null: every enrolment has a\r\n // joining date, so \"no answer\" means leave the stored one alone.\r\n joinedAt: orUndefined(values.joiningDate.trim()),\r\n };\r\n}\r\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 { DEFAULT_TIMEZONE } from './location.js';\nimport type { CreateMemberBody, UpdateMemberBody } from './member.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport type { AssignMembershipBody } from './membership.js';\nimport { bloodGroupSchema, genderSchema } from './shared.js';\nimport type { CreateStaffBody } from './staff.js';\nimport { staffRoleSchema } from './staff.js';\n\n// ─── Bounds ──────────────────────────────────────────────────────────────────\n\n/**\n * Age bounds for someone signed up at the desk. Wider than the member wizard's\n * because a gym also enrols juniors with a guardian present, and the date is\n * typed by staff rather than by the person themselves.\n */\nexport const ADD_PERSON_MIN_AGE_YEARS = 5;\nexport const ADD_PERSON_MAX_AGE_YEARS = 100;\n\n// ─── Date of birth, as three cells ───────────────────────────────────────────\n\n/**\n * The three cells joined into the `YYYY-MM-DD` the API stores, or `undefined`\n * when nothing was entered.\n *\n * Desk entry does not require a date of birth — a gym adds people with\n * whatever it has to hand — so \"blank\" is a real answer and has to survive the\n * whole way to the body as an ABSENT field rather than an empty string the\n * API would reject.\n */\nexport function isoFromDobCells(day: string, month: string, year: string): string | undefined {\n if (!day || !month || !year) return undefined;\n return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;\n}\n\n/** A stored `YYYY-MM-DD` split back into the cells, for seeding an edit form. */\nexport function dobCellsFromIso(iso: string | null | undefined): {\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n} {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(iso ?? '');\n if (!match) return { dobDay: '', dobMonth: '', dobYear: '' };\n return { dobYear: match[1] as string, dobMonth: match[2] as string, dobDay: match[3] as string };\n}\n\n// ─── Field-level rules ───────────────────────────────────────────────────────\n\nconst requiredChoice = <T extends z.ZodTypeAny>(inner: T, message: string) =>\n inner.nullable().refine((v: unknown) => v !== null, { message });\n\n// ─── One emergency-contact row ───────────────────────────────────────────────\n\n/**\n * One emergency-contact row: WHO to call, on what number, and how they are\n * related. A row is either wholly blank (ignored) or names a person.\n *\n * ── Why `name` exists now ───────────────────────────────────────────────────\n * This form used to collect a relationship and a phone with no name at all,\n * and the mapper wrote the relationship INTO `name` because\n * `emergencyContactSchema` requires one. The member's own surfaces store a real\n * name, so the two halves of the product disagreed about what the field held,\n * and the disagreement destroyed data: a member onboards with \"Priya\", the\n * owner opens Edit Member and sees Relationship prefilled \"Priya\" (the reader\n * fell back to `name`), corrects it to \"Sister\", saves — and the contact is now\n * called Sister. Whoever has to ring that number in an emergency is left with\n * a word instead of a person.\n *\n * `relationship` stays OPTIONAL, matching `emergencyContactSchema`. It is\n * useful context, not something to block a desk entry over.\n */\nexport const emergencyContactRowSchema = z\n .object({\n name: z.string().trim().max(120),\n relationship: z.string().trim().max(60),\n phone: z.string().trim(),\n })\n .superRefine((v, ctx) => {\n const blank = v.name === '' && v.relationship === '' && v.phone === '';\n if (blank) return;\n if (v.name === '') {\n ctx.addIssue({\n code: 'custom',\n path: ['name'],\n message: \"Add the contact's name\",\n });\n }\n if (!PHONE_REGEX.test(v.phone)) {\n ctx.addIssue({\n code: 'custom',\n path: ['phone'],\n message: 'Enter a valid 10-digit mobile number',\n });\n }\n });\n\nexport interface EmergencyContactRow {\n name: string;\n relationship: string;\n phone: string;\n}\n\n// ─── Shared base ─────────────────────────────────────────────────────────────\n\nconst addPersonBaseShape = {\n /** One field on screen; split into `firstName` / `lastName` on the wire. */\n name: z.string().trim().min(1, 'Name is required').max(160),\n email: z.email('Enter a valid email'),\n phone: z.string().regex(PHONE_REGEX, 'Enter a valid 10-digit mobile number'),\n /**\n * Three cells, not a native date input.\n *\n * `<input type=\"date\">` renders in the BROWSER'S locale, so the same field\n * read \"mm/dd/yyyy\" at an Indian gym's front desk — and a member's own\n * onboarding asked for the very same fact through `DobInput`'s DD/MM/YYYY\n * cells. Two surfaces collecting one fact in two formats is how a birthday\n * gets typed the wrong way round.\n *\n * All three or none: desk entry does not require a date of birth, so blank\n * is a real answer, but half a date is not.\n */\n dobDay: z.string(),\n dobMonth: z.string(),\n dobYear: z.string(),\n gender: requiredChoice(genderSchema, 'Select a gender'),\n\n /** Blank when not recorded. */\n bloodGroup: bloodGroupSchema.or(z.literal('')),\n avatarUrl: z.string(),\n\n contacts: z.array(emergencyContactRowSchema).max(MAX_EMERGENCY_CONTACTS),\n};\n\n/**\n * Date of birth is optional, but a typed one has to be real and plausible —\n * a mis-keyed year is the single most common desk-entry slip.\n */\nfunction refineDob(\n v: { dobDay: string; dobMonth: string; dobYear: string },\n ctx: z.RefinementCtx,\n): void {\n const touched = Boolean(v.dobDay || v.dobMonth || v.dobYear);\n // Nothing entered is fine — the desk records what it has.\n if (!touched) return;\n\n // Every message lands on `dobDay`, the first cell, because the three are one\n // control on screen: an error parked on `dobYear` would sit under a field\n // the form renders as a single row and reads back from `dobDay`.\n if (!v.dobDay || !v.dobMonth || v.dobYear.length !== 4) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Enter the full date of birth' });\n return;\n }\n\n const day = Number(v.dobDay);\n const month = Number(v.dobMonth);\n const year = Number(v.dobYear);\n\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'That date does not exist' });\n return;\n }\n\n const age = ageInYears(year, month, day);\n if (age < 0) {\n ctx.addIssue({\n code: 'custom',\n path: ['dobDay'],\n message: 'Date of birth cannot be in the future',\n });\n } else if (age < ADD_PERSON_MIN_AGE_YEARS || age > ADD_PERSON_MAX_AGE_YEARS) {\n ctx.addIssue({ code: 'custom', path: ['dobDay'], message: 'Check the year of birth' });\n }\n}\n\n// ─── Member form ─────────────────────────────────────────────────────────────\n\nexport const addMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n /**\n * Central-auth user id of the trainer, or `''` for none. Only trainers who\n * have CLAIMED their invite can be assigned — an unclaimed staff record has\n * no user to point at (`MemberProfile.assignedTrainerId` refs `User`).\n */\n trainerId: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n });\n\n/**\n * Adding a member asks for the person, not their paperwork.\n *\n * No address, no Aadhaar and no ID proof — on this form or on any other. A\n * front desk does not have them when somebody walks in, the member's own\n * screens never asked for them, and nothing in the product ever read one back.\n * Every surface that collected them was filling a drawer nobody opens.\n */\nexport type AddMemberFormValues = {\n name: string;\n email: string;\n phone: string;\n dobDay: string;\n dobMonth: string;\n dobYear: string;\n gender: z.infer<typeof genderSchema> | null;\n bloodGroup: z.infer<typeof bloodGroupSchema> | '';\n avatarUrl: string;\n contacts: EmergencyContactRow[];\n /** Optional — a gym with no trainers yet, or none chosen. `''` for none. */\n trainerId: string;\n};\n\n// ─── Staff form ──────────────────────────────────────────────────────────────\n\n/**\n * Staff are asked exactly what a member is asked, plus the job they do.\n *\n * The home address and the ID upload both went for the same reason: nothing in\n * the product ever showed either one back. A gym that needs an employee's\n * address for a contract has somewhere else to keep it; what it had here was a\n * field collected once and never looked at again, which is the worst thing to\n * ask anyone for.\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 dobDay: '',\n dobMonth: '',\n dobYear: '',\n gender: null,\n bloodGroup: '' as const,\n avatarUrl: '',\n} satisfies Omit<AddMemberFormValues, 'contacts' | 'trainerId'>;\n\n/** One blank row so the section renders with a contact to fill. */\nexport const EMPTY_EMERGENCY_CONTACT: EmergencyContactRow = {\n name: '',\n relationship: '',\n phone: '',\n};\n\nexport const EMPTY_ADD_MEMBER_FORM: AddMemberFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n trainerId: '',\n};\n\nexport const EMPTY_ADD_STAFF_FORM: AddStaffFormValues = {\n ...EMPTY_BASE,\n contacts: [{ ...EMPTY_EMERGENCY_CONTACT }],\n role: null,\n};\n\n// ─── Form → API body ─────────────────────────────────────────────────────────\n\n/** One \"Name\" field on screen, `firstName` + `lastName` on the wire. */\nfunction splitName(value: string): { firstName: string; lastName?: string } {\n const parts = value.trim().split(/\\s+/).filter(Boolean);\n const lastName = parts.slice(1).join(' ');\n return { firstName: parts[0] ?? '', ...(lastName ? { lastName } : {}) };\n}\n\n/** Drop a blank optional so it reads as \"not provided\" rather than `''`. */\nfunction orUndefined(value: string): string | undefined {\n const trimmed = value.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/** Phones are STORED raw — strip any display formatting back to 10 digits. */\nfunction asPhone(value: string): string {\n return value.replace(/\\D/g, '').slice(-10);\n}\n\n/**\n * The fields EVERY desk form sends.\n *\n * All three now ask exactly the same things about the PERSON; what differs is\n * what the record is FOR, so each mapper adds its own role / trainer / plan on\n * top. Address and ID proof used to be the exception here, asked by two of the\n * three; they are gone from all of them.\n */\nfunction sharedBody(v: Omit<AddMemberFormValues, 'trainerId'>) {\n // A row with nothing in it is not a contact. Anything else has already been\n // through `emergencyContactRowSchema`, which requires a name and a valid\n // phone the moment a row is touched.\n const emergencyContacts = v.contacts\n .filter((c) => c.name !== '' || c.relationship !== '' || c.phone !== '')\n .map((c) =>\n emergencyContactSchema.parse({\n name: c.name.trim(),\n phone: asPhone(c.phone),\n relationship: orUndefined(c.relationship),\n }),\n )\n .slice(0, MAX_EMERGENCY_CONTACTS);\n\n return {\n ...splitName(v.name),\n email: v.email.trim(),\n phone: asPhone(v.phone),\n // The form schema guarantees a choice was made before we get here.\n gender: v.gender ?? 'other',\n // Absent rather than empty when the desk did not record one — see\n // `isoFromDobCells`.\n dob: isoFromDobCells(v.dobDay, v.dobMonth, v.dobYear),\n bloodGroup: v.bloodGroup === '' ? undefined : v.bloodGroup,\n emergencyContacts,\n avatarUrl: orUndefined(v.avatarUrl),\n };\n}\n\n/**\n * Validated member form → `POST /members` body. `initialPlan` comes from the\n * membership step, which the form itself knows nothing about.\n */\nexport function toCreateMemberBody(\n values: AddMemberFormValues,\n initialPlan?: AssignMembershipBody,\n): CreateMemberBody {\n return {\n ...sharedBody(values),\n assignedTrainerId: orUndefined(values.trainerId),\n ...(initialPlan ? { initialPlan } : {}),\n };\n}\n\n/** Validated staff form → `POST /staff` body. */\nexport function toCreateStaffBody(values: AddStaffFormValues): CreateStaffBody {\n return {\n ...sharedBody(values),\n // The form schema guarantees a role was chosen.\n role: values.role ?? 'trainer',\n };\n}\n\n/**\n * The joining date must be a real day, and must already have happened.\n *\n * Validated here as well as on the server because this one is not a formatting\n * preference: the date anchors the member's whole billing cycle, so a typo\n * that slips through bills them on the wrong day of every month until somebody\n * notices. The server holds the same rule — this is the copy that can say so\n * before the owner presses save.\n */\nfunction refineJoiningDate(v: { joiningDate: string }, ctx: z.RefinementCtx): void {\n const raw = v.joiningDate.trim();\n // Blank is \"leave it as it is\", which is always allowed.\n if (!raw) return;\n\n const parts = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(raw);\n if (!parts) {\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'Enter a valid date' });\n return;\n }\n\n const [year, month, day] = [Number(parts[1]), Number(parts[2]), Number(parts[3])];\n if (!isRealDate(year, month, day)) {\n ctx.addIssue({ code: 'custom', path: ['joiningDate'], message: 'That date does not exist' });\n return;\n }\n\n /**\n * \"Today\" means today AT THE GYM, and the comparison is between strings.\n *\n * `en-CA` is the one common locale whose short format is already\n * `YYYY-MM-DD`, so the zone conversion and the formatting happen in one step\n * and the result sorts lexicographically — an exact calendar-day comparison\n * with no instant arithmetic in it. This mirrors `gymDayOf` on the server,\n * deliberately: a UTC day here would be one behind IST between midnight and\n * 5:30am, and would tell an owner opening the app early that TODAY is in the\n * future.\n */\n const today = new Intl.DateTimeFormat('en-CA', { timeZone: DEFAULT_TIMEZONE }).format(new Date());\n if (raw > today) {\n ctx.addIssue({\n code: 'custom',\n path: ['joiningDate'],\n message: 'A joining date cannot be in the future',\n });\n }\n}\n\n// ─── Edit member ─────────────────────────────────────────────────────────────\n\n/**\n * Editing a member asks for exactly what adding one asks for.\n *\n * It stays a separate schema and a separate mapper because the two differ in\n * what \"blank\" means. On the add form a blank optional is simply not provided;\n * on the edit form it is an instruction to CLEAR a value that is already\n * stored, so `toUpdateMemberBody` sends `null` where `toCreateMemberBody`\n * omits the field.\n */\nexport const editMemberFormSchema = z\n .object({\n ...addPersonBaseShape,\n trainerId: z.string(),\n /**\n * When the GYM says this person started training there, `YYYY-MM-DD`.\n *\n * Asked only on EDIT. At the desk the answer is always \"today\" and one\n * more field in a queue is one more thing to get wrong; the correction is\n * something the owner makes later, once they realise the app's record and\n * the gym's disagree.\n *\n * Blank means \"leave it alone\", never \"clear it\" — unlike the other\n * optional fields on this form. Every enrolment has a joining date because\n * the billing cycle is measured from one, so there is no empty state to\n * write back.\n */\n joiningDate: z.string(),\n })\n .superRefine((v, ctx) => {\n refineDob(v, ctx);\n refineJoiningDate(v, ctx);\n });\n\nexport type EditMemberFormValues = AddMemberFormValues & {\n joiningDate: 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 identity fields go along too;\n * 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 avatarUrl: values.avatarUrl.trim(),\n assignedTrainerId: orUndefined(values.trainerId) ?? null,\n // OMITTED when blank rather than sent as null: every enrolment has a\n // joining date, so \"no answer\" means leave the stored one alone.\n joinedAt: orUndefined(values.joiningDate.trim()),\n };\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export * from './location.js';
|
|
|
28
28
|
export * from './member.js';
|
|
29
29
|
export * from './member-profile.js';
|
|
30
30
|
export * from './membership.js';
|
|
31
|
+
export * from './membership-status.js';
|
|
31
32
|
export * from './membership-plan.js';
|
|
32
33
|
export * from './membership-request.js';
|
|
33
34
|
export * from './messages.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAElC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAE1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAElC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAE5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAE/B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAE9B,cAAc,WAAW,CAAC;AAE1B,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAE9B,cAAc,eAAe,CAAC;AAE9B,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AAExC,cAAc,eAAe,CAAC;AAE9B,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,cAAc,sBAAsB,CAAC;AAErC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAEnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAE7B,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -37,6 +37,7 @@ export * from './location.js';
|
|
|
37
37
|
export * from './member.js';
|
|
38
38
|
export * from './member-profile.js';
|
|
39
39
|
export * from './membership.js';
|
|
40
|
+
export * from './membership-status.js';
|
|
40
41
|
// ─── Membership ──────────────────────────────────────────────────────────────
|
|
41
42
|
export * from './membership-plan.js';
|
|
42
43
|
export * from './membership-request.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,6EAA6E;AAC7E,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,gFAAgF;AAChF,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,gFAAgF;AAChF,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,gFAAgF;AAChF,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\n * gymmonk-schema — Public API\n * ===========================\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\n * Single source of truth (SSOT) for data shapes, TypeScript types and\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\n *\n * Every domain module is re-exported through this barrel. Import names are\n * globally unique across modules, so `export *` composes without collisions.\n */\n\n// ─── Owner desk-entry FORM validation (client-side working shapes) ───────────\nexport * from './add-person-form.js';\nexport * from './announcement.js';\n// ─── Location master data (the address picker's lookup API) ───────────────\nexport * from './area.js';\nexport * from './center.js';\n// ─── Check-in QR (poster payload shared by owner, member and backend) ────────\nexport * from './chat.js';\nexport * from './check-in.js';\n// ─── Foundation ──────────────────────────────────────────────────────────────\nexport * from './codes.js';\nexport * from './common.js';\nexport * from './enrolment.js';\nexport * from './equipment.js';\n// ─── Training ────────────────────────────────────────────────────────────────\nexport * from './exercise.js';\nexport * from './feedback.js';\n// ─── Uploads ─────────────────────────────────────────────────────────────────\nexport * from './file.js';\n// ─── \"List your gym\" — a member registers the business they run ──────────────\nexport * from './gym-registration.js';\nexport * from './home.js';\nexport * from './insights.js';\n// ─── Where a place physically is (Maps link, pin, check-in position) ─────────\nexport * from './location.js';\n// ─── People ──────────────────────────────────────────────────────────────────\nexport * from './member.js';\nexport * from './member-profile.js';\nexport * from './membership.js';\n// ─── Membership ──────────────────────────────────────────────────────────────\nexport * from './membership-plan.js';\nexport * from './membership-request.js';\n// ─── API result messages + error codes ───────────────────────────────────────\nexport * from './messages.js';\n// ─── Engagement & analytics ──────────────────────────────────────────────────\nexport * from './notification.js';\nexport * from './onboarding.js';\n// ─── Onboarding wizard FORM validation (client-side working shapes) ──────────\nexport * from './onboarding-form.js';\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\nexport * from './organisation.js';\nexport * from './owner-profile.js';\n// ─── Platform console (people + gyms directories, role keys) ─────────────────\nexport * from './plan-abilities.js';\nexport * from './plan-features-data.js';\nexport * from './plan-limits.js';\nexport * from './platform.js';\nexport * from './pricing.js';\n// ─── Member \"edit my profile\" FORM validation ────────────────────────────────\nexport * from './profile-edit-form.js';\nexport * from './saas-catalog.js';\nexport * from './session.js';\nexport * from './shared.js';\nexport * from './social.js';\nexport * from './staff.js';\nexport * from './subscription.js';\nexport * from './user.js';\nexport * from './workout-plan.js';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,6EAA6E;AAC7E,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,WAAW,CAAC;AAC1B,gFAAgF;AAChF,cAAc,uBAAuB,CAAC;AACtC,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,wBAAwB,CAAC;AACvC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,cAAc,yBAAyB,CAAC;AACxC,gFAAgF;AAChF,cAAc,eAAe,CAAC;AAC9B,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAChC,gFAAgF;AAChF,cAAc,sBAAsB,CAAC;AACrC,gFAAgF;AAChF,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,gFAAgF;AAChF,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,gFAAgF;AAChF,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,mBAAmB,CAAC;AAClC,cAAc,WAAW,CAAC;AAC1B,cAAc,mBAAmB,CAAC","sourcesContent":["/**\r\n * gymmonk-schema — Public API\r\n * ===========================\r\n * Shared Zod schemas, enums and domain types for GymMonk (fitness SaaS).\r\n * Single source of truth (SSOT) for data shapes, TypeScript types and\r\n * validation, consumed by both gymmonk-backend and gymmonk-web-client.\r\n *\r\n * Every domain module is re-exported through this barrel. Import names are\r\n * globally unique across modules, so `export *` composes without collisions.\r\n */\r\n\r\n// ─── Owner desk-entry FORM validation (client-side working shapes) ───────────\r\nexport * from './add-person-form.js';\r\nexport * from './announcement.js';\r\n// ─── Location master data (the address picker's lookup API) ───────────────\r\nexport * from './area.js';\r\nexport * from './center.js';\r\n// ─── Check-in QR (poster payload shared by owner, member and backend) ────────\r\nexport * from './chat.js';\r\nexport * from './check-in.js';\r\n// ─── Foundation ──────────────────────────────────────────────────────────────\r\nexport * from './codes.js';\r\nexport * from './common.js';\r\nexport * from './enrolment.js';\r\nexport * from './equipment.js';\r\n// ─── Training ────────────────────────────────────────────────────────────────\r\nexport * from './exercise.js';\r\nexport * from './feedback.js';\r\n// ─── Uploads ─────────────────────────────────────────────────────────────────\r\nexport * from './file.js';\r\n// ─── \"List your gym\" — a member registers the business they run ──────────────\r\nexport * from './gym-registration.js';\r\nexport * from './home.js';\r\nexport * from './insights.js';\r\n// ─── Where a place physically is (Maps link, pin, check-in position) ─────────\r\nexport * from './location.js';\r\n// ─── People ──────────────────────────────────────────────────────────────────\r\nexport * from './member.js';\r\nexport * from './member-profile.js';\r\nexport * from './membership.js';\r\nexport * from './membership-status.js';\r\n// ─── Membership ──────────────────────────────────────────────────────────────\r\nexport * from './membership-plan.js';\r\nexport * from './membership-request.js';\r\n// ─── API result messages + error codes ───────────────────────────────────────\r\nexport * from './messages.js';\r\n// ─── Engagement & analytics ──────────────────────────────────────────────────\r\nexport * from './notification.js';\r\nexport * from './onboarding.js';\r\n// ─── Onboarding wizard FORM validation (client-side working shapes) ──────────\r\nexport * from './onboarding-form.js';\r\n// ─── Gym structure & onboarding ──────────────────────────────────────────────\r\nexport * from './organisation.js';\r\nexport * from './owner-profile.js';\r\n// ─── Platform console (people + gyms directories, role keys) ─────────────────\r\nexport * from './plan-abilities.js';\r\nexport * from './plan-features-data.js';\r\nexport * from './plan-limits.js';\r\nexport * from './platform.js';\r\nexport * from './pricing.js';\r\n// ─── Member \"edit my profile\" FORM validation ────────────────────────────────\r\nexport * from './profile-edit-form.js';\r\nexport * from './saas-catalog.js';\r\nexport * from './session.js';\r\nexport * from './shared.js';\r\nexport * from './social.js';\r\nexport * from './staff.js';\r\nexport * from './subscription.js';\r\nexport * from './user.js';\r\nexport * from './workout-plan.js';\r\n"]}
|
package/dist/member.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export declare const memberRosterItemSchema: z.ZodObject<{
|
|
|
42
42
|
cancelled: "cancelled";
|
|
43
43
|
}>>;
|
|
44
44
|
endDate: z.ZodNullable<z.ZodString>;
|
|
45
|
+
pausedFrom: z.ZodNullable<z.ZodString>;
|
|
46
|
+
pausedTo: z.ZodNullable<z.ZodString>;
|
|
45
47
|
lastVisitAt: z.ZodNullable<z.ZodISODateTime>;
|
|
46
48
|
balanceDueInr: z.ZodNumber;
|
|
47
49
|
overdueDays: z.ZodNullable<z.ZodNumber>;
|
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;AAuBxB,kDAAkD;AAClD,eAAO,MAAM,mBAAmB;;;;EAAyC,CAAC;AAC1E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE,eAAO,MAAM,sBAAsB
|
|
1
|
+
{"version":3,"file":"member.d.ts","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"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"}
|
package/dist/member.js
CHANGED
|
@@ -47,6 +47,19 @@ export const memberRosterItemSchema = z.object({
|
|
|
47
47
|
planName: z.string().nullable(),
|
|
48
48
|
status: membershipStatusSchema.nullable(),
|
|
49
49
|
endDate: isoDateSchema.nullable(),
|
|
50
|
+
/**
|
|
51
|
+
* The pause window, carried on the ROW rather than looked up per member.
|
|
52
|
+
*
|
|
53
|
+
* A pause is a window that ends by itself — nothing resumes it — so
|
|
54
|
+
* `status: 'paused'` stays written in the database long after the member is
|
|
55
|
+
* back. Without these two dates the roster can only see the stale column,
|
|
56
|
+
* and it showed exactly that: a "Paused" pill and exclusion from
|
|
57
|
+
* `?filter=active` on somebody the door had already started letting in, while
|
|
58
|
+
* the header count beside them — which reads the same window in Mongo — said
|
|
59
|
+
* they were active. Two numbers on one screen that could not both be true.
|
|
60
|
+
*/
|
|
61
|
+
pausedFrom: isoDateSchema.nullable(),
|
|
62
|
+
pausedTo: isoDateSchema.nullable(),
|
|
50
63
|
lastVisitAt: isoDateTimeSchema.nullable(),
|
|
51
64
|
balanceDueInr: priceInrSchema,
|
|
52
65
|
overdueDays: z.number().int().nullable(),
|
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":["/**\r\n * gymmonk-schema — Member management (owner console)\r\n * ==================================================\r\n * Owner-facing roster + detail projections (User × MemberProfile × Membership)\r\n * and the \"add member\" provisioning payload. Adding a member provisions a real\r\n * central-auth account, so the result carries the login email + temp password.\r\n *\r\n * @module gymmonk-schema/member\r\n */\r\n\r\nimport { z } from 'zod';\r\nimport { gymMemberCodeSchema } from './codes.js';\r\nimport {\r\n isoDateSchema,\r\n isoDateTimeSchema,\r\n objectIdSchema,\r\n optionalTrimmedString,\r\n paginationSchema,\r\n phoneSchema,\r\n priceInrSchema,\r\n} from './common.js';\r\nimport { enrolmentStatusSchema } from './enrolment.js';\r\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\r\nimport {\r\n assignMembershipBodySchema,\r\n membershipSchema,\r\n membershipStatusSchema,\r\n membershipTermSchema,\r\n} from './membership.js';\r\nimport { bloodGroupSchema, genderSchema } from './shared.js';\r\n\r\n// ─── Segments ────────────────────────────────────────────────────────────────\r\n\r\n/** Attention buckets surfaced on roster cards. */\r\nexport const memberSegmentSchema = z.enum(['expiring', 'new', 'at-risk']);\r\nexport type MemberSegment = z.infer<typeof memberSegmentSchema>;\r\n\r\n// ─── Roster item (list projection) ───────────────────────────────────────────\r\n\r\nexport const memberRosterItemSchema = z.object({\r\n /** The gym record id — always present, even before the member claims. */\r\n enrolmentId: objectIdSchema,\r\n /** Null until claimed; the roster shows an \"Invited\" pill in that state. */\r\n userId: objectIdSchema.nullable(),\r\n recordStatus: enrolmentStatusSchema,\r\n membershipId: objectIdSchema.nullable(),\r\n gymMemberCode: gymMemberCodeSchema,\r\n name: z.string(),\r\n avatarUrl: z.string().nullable(),\r\n verified: z.boolean(),\r\n /**\r\n * Contact details, on the ROSTER row rather than only the detail record.\r\n *\r\n * The card's whole job when someone is overdue is to let the owner chase it\r\n * without leaving the list — call, WhatsApp, email. Those are `tel:` /\r\n * `wa.me` / `mailto:` links, so the row cannot build them without the raw\r\n * values, and fetching the full member just to dial them would be one\r\n * request per card.\r\n *\r\n * Nullable because an invited record may have only one of the two: the gym\r\n * creates it with whatever it has, and the member fills in the rest on claim.\r\n * Phone is RAW 10 digits — `+91` is display-only, never stored.\r\n */\r\n phone: z.string().nullable(),\r\n email: z.email().nullable(),\r\n planName: z.string().nullable(),\r\n status: membershipStatusSchema.nullable(),\r\n endDate: isoDateSchema.nullable(),\r\n lastVisitAt: isoDateTimeSchema.nullable(),\r\n balanceDueInr: priceInrSchema,\r\n overdueDays: z.number().int().nullable(),\r\n segments: z.array(memberSegmentSchema),\r\n\r\n /**\r\n * When the gym record was created — NOT when the person claimed it. The\r\n * roster's \"Joined Date\" filter groups by how long someone has been a member\r\n * of the gym, which starts at the desk, not at the login.\r\n */\r\n joinedAt: isoDateTimeSchema,\r\n\r\n /**\r\n * Check-ins in the trailing 14 days, used by the roster's attendance filters\r\n * to separate a no-show from an irregular from a regular.\r\n *\r\n * Counted rather than derived from `lastVisitAt`, because the two answer\r\n * different questions: someone who came once yesterday and someone who comes\r\n * daily share a last visit but are not the same member to chase.\r\n */\r\n visits14d: z.number().int().nonnegative(),\r\n\r\n /** Assigned trainer, so the roster can be filtered down to one trainer's people. */\r\n trainerId: objectIdSchema.nullable(),\r\n trainerName: z.string().nullable(),\r\n});\r\n\r\n/** Trailing window the roster's attendance filters and `visits14d` are measured over. */\r\nexport const ATTENDANCE_WINDOW_DAYS = 14;\r\nexport type MemberRosterItem = z.infer<typeof memberRosterItemSchema>;\r\n\r\n/** Header counts for the roster. */\r\nexport const memberListSummarySchema = z.object({\r\n total: z.number().int().nonnegative(),\r\n active: z.number().int().nonnegative(),\r\n expiring: z.number().int().nonnegative(),\r\n inactive: z.number().int().nonnegative(),\r\n});\r\nexport type MemberListSummary = z.infer<typeof memberListSummarySchema>;\r\n\r\n// ─── List query ──────────────────────────────────────────────────────────────\r\n\r\nexport const memberFilterSchema = z.enum(['all', 'active', 'inactive', 'verified']);\r\nexport type MemberFilter = z.infer<typeof memberFilterSchema>;\r\n\r\nexport const memberSortSchema = z.enum(['recent', 'az', 'expiry', 'dues']);\r\nexport type MemberSort = z.infer<typeof memberSortSchema>;\r\n\r\nexport const memberListQuerySchema = paginationSchema.extend({\r\n centerId: objectIdSchema.optional(),\r\n filter: memberFilterSchema.default('all'),\r\n sort: memberSortSchema.default('recent'),\r\n q: optionalTrimmedString(120),\r\n});\r\nexport type MemberListQuery = z.infer<typeof memberListQuerySchema>;\r\n\r\n// ─── Detail projection ───────────────────────────────────────────────────────\r\n\r\n/** The identity slice a projection pulls from the auth `User`. */\r\nexport const memberUserRefSchema = z.object({\r\n /** Null while the record is still `invited` (no central-auth account linked). */\r\n id: objectIdSchema.nullable(),\r\n name: z.string(),\r\n email: z.string(),\r\n phone: z.string().nullable(),\r\n avatarUrl: z.string().nullable(),\r\n verified: z.boolean(),\r\n});\r\nexport type MemberUserRef = z.infer<typeof memberUserRefSchema>;\r\n\r\nexport const memberDetailSchema = z.object({\r\n enrolmentId: objectIdSchema,\r\n recordStatus: enrolmentStatusSchema,\r\n /**\r\n * Identity from central auth once claimed. Before that the gym still knows the\r\n * person (name / contact captured at the desk), so this carries the invited\r\n * details with `id: null`.\r\n */\r\n user: memberUserRefSchema,\r\n gymMemberCode: gymMemberCodeSchema,\r\n gender: genderSchema.nullable(),\r\n dob: isoDateSchema.nullable(),\r\n bloodGroup: bloodGroupSchema.nullable(),\r\n emergencyContacts: z.array(emergencyContactSchema),\r\n joinedAt: isoDateTimeSchema,\r\n lastVisitAt: isoDateTimeSchema.nullable(),\r\n assignedWorkoutPlanId: objectIdSchema.nullable(),\r\n assignedTrainerId: objectIdSchema.nullable(),\r\n /**\r\n * The member's current membership, in FULL rather than by id.\r\n *\r\n * The profile screen shows the plan name, its billing cycle, the lifecycle\r\n * status pill and the expiry date all at once, and every one of those lives\r\n * here. Sending only the id forced the owner console to re-query the roster\r\n * for the same member just to read the plan columns back, and to match the\r\n * plan by NAME to find its perk icons — both of which this removes.\r\n */\r\n membership: membershipSchema.nullable(),\r\n /**\r\n * Still owed on the current term: its price less what has been recorded as\r\n * collected. Zero when settled, when the membership was cancelled, or when\r\n * there is no membership at all. Same definition as the roster's column, so\r\n * the profile and the list can never disagree about who owes money.\r\n */\r\n balanceDueInr: z.number().int().nonnegative(),\r\n /**\r\n * The term the NEXT confirmed payment will cover, so the owner reads it\r\n * before deciding rather than discovering it afterwards.\r\n *\r\n * Null when the answer is not yet knowable: no membership has ever been\r\n * assigned, so there is no variant duration to measure a term with. It is\r\n * deliberately computed here rather than in the client — the cycle depends\r\n * on the joining date, the enrolment's creation month and the terms already\r\n * sold, and a client that recomputed it from a partial view would show a\r\n * different month than the one the server is about to write.\r\n */\r\n nextTerm: membershipTermSchema.nullable(),\r\n});\r\nexport type MemberDetail = z.infer<typeof memberDetailSchema>;\r\n\r\n// ─── Add member (provisioning) ───────────────────────────────────────────────\r\n\r\nexport const createMemberBodySchema = z.object({\r\n firstName: z.string().trim().min(1, 'First name is required').max(80),\r\n lastName: optionalTrimmedString(80),\r\n email: z.email('A valid email is required'),\r\n phone: phoneSchema,\r\n gender: genderSchema,\r\n dob: isoDateSchema.optional(),\r\n bloodGroup: bloodGroupSchema.optional(),\r\n emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\r\n avatarUrl: optionalTrimmedString(2000),\r\n assignedTrainerId: objectIdSchema.optional(),\r\n /** Optionally assign a plan in the same step. */\r\n initialPlan: assignMembershipBodySchema.optional(),\r\n});\r\nexport type CreateMemberBody = z.infer<typeof createMemberBodySchema>;\r\n\r\n/**\r\n * Edit a member's record from the owner console.\r\n *\r\n * Every field is optional — the console sends what the form holds, and a field\r\n * left out is left alone.\r\n *\r\n * ## What the gym may and may not change\r\n * `firstName` / `lastName` / `email` / `phone` are the INVITE details, and the\r\n * server accepts them only while the record is unclaimed. Once the member has\r\n * linked their central-auth account, that account is the source of truth for\r\n * their identity and is shared with the other products — a gym must not be able\r\n * to rewrite someone's name or email out from under them. The server ignores\r\n * those four on a claimed record rather than failing the whole request, so an\r\n * owner correcting a blood group is never blocked by a name they cannot change.\r\n *\r\n * Nullable fields (`dob`, `bloodGroup`, `assignedTrainerId`) accept `null` to\r\n * CLEAR a value, which `undefined` cannot express.\r\n */\r\nexport const updateMemberBodySchema = createMemberBodySchema\r\n .omit({\r\n initialPlan: true,\r\n dob: true,\r\n bloodGroup: true,\r\n assignedTrainerId: true,\r\n })\r\n .partial()\r\n .extend({\r\n dob: isoDateSchema.nullish(),\r\n bloodGroup: bloodGroupSchema.nullish(),\r\n assignedTrainerId: objectIdSchema.nullish(),\r\n /**\r\n * The date the GYM says this person started training there.\r\n *\r\n * Editable because the app's record and the gym's record are different\r\n * facts and routinely disagree: somebody who has trained since March is\r\n * added to GymMonk in July, and until now their joining date was silently\r\n * the day the desk got round to typing them in. That date is not a\r\n * cosmetic label — it anchors the billing cycle, so a wrong one bills the\r\n * member on the wrong day of every month for as long as they stay.\r\n *\r\n * Deliberately unbounded in the past: it records something that already\r\n * happened, and a gym may well be older than this app. The BILLING floor\r\n * is a separate rule and is enforced where terms are derived, so an\r\n * honest March joining date can never be turned into a March invoice.\r\n */\r\n joinedAt: isoDateSchema.optional(),\r\n });\r\nexport type UpdateMemberBody = z.infer<typeof updateMemberBodySchema>;\r\n\r\n// ─── Invite & claim ──────────────────────────────────────────────────────────\r\n\r\n/**\r\n * The invite a gym hands to a new member.\r\n *\r\n * GymMonk never mints credentials for anyone: central auth is the SSOT for\r\n * identity, and an account there is shared across products. So adding a member\r\n * creates a gym record only, plus an unguessable invite code. The member\r\n * registers/logs in themselves through central auth (exactly as in jansathi) and\r\n * then CLAIMS the record. The owner shares `url` over WhatsApp or shows the QR.\r\n */\r\nexport const memberInviteSchema = z.object({\r\n /** Single-use, unguessable. Regenerating an invite invalidates the old one. */\r\n code: z.string(),\r\n /** Absolute claim URL the owner shares (also encoded into the QR). */\r\n url: z.string(),\r\n /** The address the member MUST register with for the claim to succeed. */\r\n email: z.string(),\r\n /** WhatsApp number to share on, when the gym recorded one. */\r\n phone: z.string().nullable(),\r\n expiresAt: isoDateTimeSchema,\r\n});\r\nexport type MemberInvite = z.infer<typeof memberInviteSchema>;\r\n\r\n/** Result of adding a member: the gym record + the invite to hand over. */\r\nexport const provisionResultSchema = z.object({\r\n /** The new member record (NOT a central-auth user — none is created). */\r\n enrolmentId: objectIdSchema,\r\n code: z.string(),\r\n invite: memberInviteSchema,\r\n /**\r\n * The membership created from `initialPlan`, or `null`.\r\n *\r\n * Null means one of two DIFFERENT things, and the caller can tell them apart\r\n * because it knows whether it sent `initialPlan`: no plan was chosen, or the\r\n * chosen plan could not be assigned. Adding a member never fails over the\r\n * second case — the person is on the roster either way — so this is how the\r\n * owner finds out the plan still needs assigning rather than assuming it took.\r\n */\r\n membership: membershipSchema.nullable(),\r\n});\r\nexport type ProvisionResult = z.infer<typeof provisionResultSchema>;\r\n\r\n/**\r\n * What the claim screen shows BEFORE the member authenticates, so they know\r\n * which address to register with. Public (the code is the bearer secret), and\r\n * deliberately minimal — gym name, member code, and the expected email.\r\n */\r\nexport const invitePreviewSchema = z.object({\r\n gymName: z.string(),\r\n gymMemberCode: gymMemberCodeSchema,\r\n /** Shown verbatim: the member must know exactly which address to use. */\r\n email: z.string(),\r\n expiresAt: isoDateTimeSchema,\r\n /** True once claimed / expired, so the screen can explain why it failed. */\r\n claimed: z.boolean(),\r\n expired: z.boolean(),\r\n});\r\nexport type InvitePreview = z.infer<typeof invitePreviewSchema>;\r\n\r\n/**\r\n * Claim an invite as the authenticated caller. The server links the record only\r\n * when the code is valid AND the caller's central-auth email matches the address\r\n * the invite was issued to.\r\n */\r\nexport const claimInviteBodySchema = z.object({\r\n code: z.string().trim().min(1),\r\n});\r\nexport type ClaimInviteBody = z.infer<typeof claimInviteBodySchema>;\r\n\r\n// ─── Mutations ───────────────────────────────────────────────────────────────\r\n\r\nexport const setMemberStatusBodySchema = z.object({ active: z.boolean() });\r\nexport type SetMemberStatusBody = z.infer<typeof setMemberStatusBodySchema>;\r\n\r\nexport const assignTrainerBodySchema = z.object({\r\n trainerId: objectIdSchema.nullable(),\r\n});\r\nexport type AssignTrainerBody = z.infer<typeof assignTrainerBodySchema>;\r\n\r\nexport const assignWorkoutPlanBodySchema = z.object({\r\n planId: objectIdSchema.nullable(),\r\n});\r\nexport type AssignWorkoutPlanBody = z.infer<typeof assignWorkoutPlanBodySchema>;\r\n\r\n/** Remove a member from the gym — echoes the member code to confirm. */\r\nexport const deleteMemberBodySchema = z.object({\r\n confirmationCode: z.string().trim().min(1),\r\n});\r\nexport type DeleteMemberBody = z.infer<typeof deleteMemberBodySchema>;\r\n"]}
|
|
1
|
+
{"version":3,"file":"member.js","sourceRoot":"","sources":["../src/member.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,gBAAgB,EAChB,WAAW,EACX,cAAc,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EACL,0BAA0B,EAC1B,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7D,gFAAgF;AAEhF,kDAAkD;AAClD,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;AAG1E,gFAAgF;AAEhF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,yEAAyE;IACzE,WAAW,EAAE,cAAc;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE;IACjC,YAAY,EAAE,qBAAqB;IACnC,YAAY,EAAE,cAAc,CAAC,QAAQ,EAAE;IACvC,aAAa,EAAE,mBAAmB;IAClC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;IACrB;;;;;;;;;;;;OAYG;IACH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IAC3B,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC/B,MAAM,EAAE,sBAAsB,CAAC,QAAQ,EAAE;IACzC,OAAO,EAAE,aAAa,CAAC,QAAQ,EAAE;IAEjC;;;;;;;;;;OAUG;IACH,UAAU,EAAE,aAAa,CAAC,QAAQ,EAAE;IACpC,QAAQ,EAAE,aAAa,CAAC,QAAQ,EAAE;IAElC,WAAW,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACzC,aAAa,EAAE,cAAc;IAC7B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACxC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC;IAEtC;;;;OAIG;IACH,QAAQ,EAAE,iBAAiB;IAE3B;;;;;;;OAOG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAEzC,oFAAoF;IACpF,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAEH,yFAAyF;AACzF,MAAM,CAAC,MAAM,sBAAsB,GAAG,EAAE,CAAC;AAGzC,oCAAoC;AACpC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACrC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACtC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IACxC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;CACzC,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;AAGpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AAG3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAC3D,QAAQ,EAAE,cAAc,CAAC,QAAQ,EAAE;IACnC,MAAM,EAAE,kBAAkB,CAAC,OAAO,CAAC,KAAK,CAAC;IACzC,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,QAAQ,CAAC;IACxC,CAAC,EAAE,qBAAqB,CAAC,GAAG,CAAC;CAC9B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,kEAAkE;AAClE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,iFAAiF;IACjF,EAAE,EAAE,cAAc,CAAC,QAAQ,EAAE;IAC7B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;CACtB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,WAAW,EAAE,cAAc;IAC3B,YAAY,EAAE,qBAAqB;IACnC;;;;OAIG;IACH,IAAI,EAAE,mBAAmB;IACzB,aAAa,EAAE,mBAAmB;IAClC,MAAM,EAAE,YAAY,CAAC,QAAQ,EAAE;IAC/B,GAAG,EAAE,aAAa,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC;IAClD,QAAQ,EAAE,iBAAiB;IAC3B,WAAW,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACzC,qBAAqB,EAAE,cAAc,CAAC,QAAQ,EAAE;IAChD,iBAAiB,EAAE,cAAc,CAAC,QAAQ,EAAE;IAC5C;;;;;;;;OAQG;IACH,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC;;;;;OAKG;IACH,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;IAC7C;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,oBAAoB,CAAC,QAAQ,EAAE;CAC1C,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC;IACrE,QAAQ,EAAE,qBAAqB,CAAC,EAAE,CAAC;IACnC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,2BAA2B,CAAC;IAC3C,KAAK,EAAE,WAAW;IAClB,MAAM,EAAE,YAAY;IACpB,GAAG,EAAE,aAAa,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;IACvC,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC1F,SAAS,EAAE,qBAAqB,CAAC,IAAI,CAAC;IACtC,iBAAiB,EAAE,cAAc,CAAC,QAAQ,EAAE;IAC5C,iDAAiD;IACjD,WAAW,EAAE,0BAA0B,CAAC,QAAQ,EAAE;CACnD,CAAC,CAAC;AAGH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,sBAAsB;KACzD,IAAI,CAAC;IACJ,WAAW,EAAE,IAAI;IACjB,GAAG,EAAE,IAAI;IACT,UAAU,EAAE,IAAI;IAChB,iBAAiB,EAAE,IAAI;CACxB,CAAC;KACD,OAAO,EAAE;KACT,MAAM,CAAC;IACN,GAAG,EAAE,aAAa,CAAC,OAAO,EAAE;IAC5B,UAAU,EAAE,gBAAgB,CAAC,OAAO,EAAE;IACtC,iBAAiB,EAAE,cAAc,CAAC,OAAO,EAAE;IAC3C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,EAAE,aAAa,CAAC,QAAQ,EAAE;CACnC,CAAC,CAAC;AAGL,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,+EAA+E;IAC/E,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,sEAAsE;IACtE,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,0EAA0E;IAC1E,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,8DAA8D;IAC9D,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,SAAS,EAAE,iBAAiB;CAC7B,CAAC,CAAC;AAGH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,yEAAyE;IACzE,WAAW,EAAE,cAAc;IAC3B,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,MAAM,EAAE,kBAAkB;IAC1B;;;;;;;;OAQG;IACH,UAAU,EAAE,gBAAgB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,aAAa,EAAE,mBAAmB;IAClC,yEAAyE;IACzE,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,SAAS,EAAE,iBAAiB;IAC5B,4EAA4E;IAC5E,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;CACrB,CAAC,CAAC;AAGH;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC/B,CAAC,CAAC;AAGH,gFAAgF;AAEhF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AAG3E,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;CACrC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,MAAM,EAAE,cAAc,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAC;AAGH,wEAAwE;AACxE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,gBAAgB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC3C,CAAC,CAAC","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 { gymMemberCodeSchema } from './codes.js';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n phoneSchema,\n priceInrSchema,\n} from './common.js';\nimport { enrolmentStatusSchema } from './enrolment.js';\nimport { emergencyContactSchema, MAX_EMERGENCY_CONTACTS } from './member-profile.js';\nimport {\n assignMembershipBodySchema,\n membershipSchema,\n membershipStatusSchema,\n membershipTermSchema,\n} from './membership.js';\nimport { 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 enrolmentId: objectIdSchema,\n /** Null until claimed; the roster shows an \"Invited\" pill in that state. */\n userId: objectIdSchema.nullable(),\n recordStatus: enrolmentStatusSchema,\n membershipId: objectIdSchema.nullable(),\n gymMemberCode: gymMemberCodeSchema,\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 status: membershipStatusSchema.nullable(),\n endDate: isoDateSchema.nullable(),\n\n /**\n * The pause window, carried on the ROW rather than looked up per member.\n *\n * A pause is a window that ends by itself — nothing resumes it — so\n * `status: 'paused'` stays written in the database long after the member is\n * back. Without these two dates the roster can only see the stale column,\n * and it showed exactly that: a \"Paused\" pill and exclusion from\n * `?filter=active` on somebody the door had already started letting in, while\n * the header count beside them — which reads the same window in Mongo — said\n * they were active. Two numbers on one screen that could not both be true.\n */\n pausedFrom: isoDateSchema.nullable(),\n pausedTo: isoDateSchema.nullable(),\n\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 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 enrolmentId: objectIdSchema,\n recordStatus: enrolmentStatusSchema,\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 gymMemberCode: gymMemberCodeSchema,\n gender: genderSchema.nullable(),\n dob: isoDateSchema.nullable(),\n bloodGroup: bloodGroupSchema.nullable(),\n emergencyContacts: z.array(emergencyContactSchema),\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 /**\n * The term the NEXT confirmed payment will cover, so the owner reads it\n * before deciding rather than discovering it afterwards.\n *\n * Null when the answer is not yet knowable: no membership has ever been\n * assigned, so there is no variant duration to measure a term with. It is\n * deliberately computed here rather than in the client — the cycle depends\n * on the joining date, the enrolment's creation month and the terms already\n * sold, and a client that recomputed it from a partial view would show a\n * different month than the one the server is about to write.\n */\n nextTerm: membershipTermSchema.nullable(),\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 emergencyContacts: z.array(emergencyContactSchema).max(MAX_EMERGENCY_CONTACTS).default([]),\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`, `assignedTrainerId`) accept `null` to\n * CLEAR a value, which `undefined` cannot express.\n */\nexport const updateMemberBodySchema = createMemberBodySchema\n .omit({\n initialPlan: true,\n dob: true,\n bloodGroup: true,\n assignedTrainerId: true,\n })\n .partial()\n .extend({\n dob: isoDateSchema.nullish(),\n bloodGroup: bloodGroupSchema.nullish(),\n assignedTrainerId: objectIdSchema.nullish(),\n /**\n * The date the GYM says this person started training there.\n *\n * Editable because the app's record and the gym's record are different\n * facts and routinely disagree: somebody who has trained since March is\n * added to GymMonk in July, and until now their joining date was silently\n * the day the desk got round to typing them in. That date is not a\n * cosmetic label — it anchors the billing cycle, so a wrong one bills the\n * member on the wrong day of every month for as long as they stay.\n *\n * Deliberately unbounded in the past: it records something that already\n * happened, and a gym may well be older than this app. The BILLING floor\n * is a separate rule and is enforced where terms are derived, so an\n * honest March joining date can never be turned into a March invoice.\n */\n joinedAt: isoDateSchema.optional(),\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 enrolmentId: 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 gymMemberCode: gymMemberCodeSchema,\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"]}
|
|
@@ -23,9 +23,9 @@ export type PlanAccent = z.infer<typeof planAccentSchema>;
|
|
|
23
23
|
* 1m/3m/6m/1y list it replaces forced them to.
|
|
24
24
|
*/
|
|
25
25
|
export declare const durationUnitSchema: z.ZodEnum<{
|
|
26
|
+
day: "day";
|
|
26
27
|
year: "year";
|
|
27
28
|
month: "month";
|
|
28
|
-
day: "day";
|
|
29
29
|
week: "week";
|
|
30
30
|
}>;
|
|
31
31
|
export type DurationUnit = z.infer<typeof durationUnitSchema>;
|
|
@@ -47,9 +47,9 @@ export declare const planVariantSchema: z.ZodObject<{
|
|
|
47
47
|
id: z.ZodString;
|
|
48
48
|
durationCount: z.ZodNumber;
|
|
49
49
|
durationUnit: z.ZodEnum<{
|
|
50
|
+
day: "day";
|
|
50
51
|
year: "year";
|
|
51
52
|
month: "month";
|
|
52
|
-
day: "day";
|
|
53
53
|
week: "week";
|
|
54
54
|
}>;
|
|
55
55
|
originalPriceInr: z.ZodNumber;
|
|
@@ -70,9 +70,9 @@ export declare const checkedPlanVariantSchema: z.ZodObject<{
|
|
|
70
70
|
id: z.ZodString;
|
|
71
71
|
durationCount: z.ZodNumber;
|
|
72
72
|
durationUnit: z.ZodEnum<{
|
|
73
|
+
day: "day";
|
|
73
74
|
year: "year";
|
|
74
75
|
month: "month";
|
|
75
|
-
day: "day";
|
|
76
76
|
week: "week";
|
|
77
77
|
}>;
|
|
78
78
|
originalPriceInr: z.ZodNumber;
|
|
@@ -617,9 +617,9 @@ export declare const createMembershipPlanBodySchema: z.ZodObject<{
|
|
|
617
617
|
id: z.ZodString;
|
|
618
618
|
durationCount: z.ZodNumber;
|
|
619
619
|
durationUnit: z.ZodEnum<{
|
|
620
|
+
day: "day";
|
|
620
621
|
year: "year";
|
|
621
622
|
month: "month";
|
|
622
|
-
day: "day";
|
|
623
623
|
week: "week";
|
|
624
624
|
}>;
|
|
625
625
|
originalPriceInr: z.ZodNumber;
|
|
@@ -714,9 +714,9 @@ export declare const updateMembershipPlanBodySchema: z.ZodObject<{
|
|
|
714
714
|
id: z.ZodString;
|
|
715
715
|
durationCount: z.ZodNumber;
|
|
716
716
|
durationUnit: z.ZodEnum<{
|
|
717
|
+
day: "day";
|
|
717
718
|
year: "year";
|
|
718
719
|
month: "month";
|
|
719
|
-
day: "day";
|
|
720
720
|
week: "week";
|
|
721
721
|
}>;
|
|
722
722
|
originalPriceInr: z.ZodNumber;
|
|
@@ -814,9 +814,9 @@ export declare const membershipPlanSchema: z.ZodObject<{
|
|
|
814
814
|
id: z.ZodString;
|
|
815
815
|
durationCount: z.ZodNumber;
|
|
816
816
|
durationUnit: z.ZodEnum<{
|
|
817
|
+
day: "day";
|
|
817
818
|
year: "year";
|
|
818
819
|
month: "month";
|
|
819
|
-
day: "day";
|
|
820
820
|
week: "week";
|
|
821
821
|
}>;
|
|
822
822
|
originalPriceInr: z.ZodNumber;
|