gymmonk-schema 0.49.0 → 0.51.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.
@@ -99,6 +99,35 @@ export declare const membershipRequestQuerySchema: z.ZodObject<{
99
99
  }>>;
100
100
  }, z.core.$strip>;
101
101
  export type MembershipRequestQuery = z.infer<typeof membershipRequestQuerySchema>;
102
+ /**
103
+ * Where ONE request stands — the answer a notification card redraws itself from.
104
+ *
105
+ * ── Why this exists as its own reading ──────────────────────────────────────
106
+ * A join request can be answered from the notification feed, from the requests
107
+ * inbox, or by a different member of staff on a different phone. Whichever
108
+ * happens, every OTHER surface showing that request is now stale — and the one
109
+ * showing Accept / Decline buttons is the dangerous kind of stale, because the
110
+ * buttons still look live and the second tap fails.
111
+ *
112
+ * A card that tracks its own decision in component state only knows about taps
113
+ * made in THAT session: reload the feed, or navigate away and back, and the
114
+ * buttons return on a request that was settled days ago. This is the
115
+ * server-side truth that stops that, so a settled request reads as settled
116
+ * everywhere, forever, no matter who settled it.
117
+ *
118
+ * Deliberately just the decision. The notification already carries the member's
119
+ * name and avatar, so this must not become a second, thinner way to read a
120
+ * person's details.
121
+ */
122
+ export declare const membershipRequestStatusResultSchema: z.ZodObject<{
123
+ status: z.ZodEnum<{
124
+ pending: "pending";
125
+ rejected: "rejected";
126
+ approved: "approved";
127
+ }>;
128
+ decidedAt: z.ZodNullable<z.ZodISODateTime>;
129
+ }, z.core.$strip>;
130
+ export type MembershipRequestStatusResult = z.infer<typeof membershipRequestStatusResultSchema>;
102
131
  /**
103
132
  * What the member's own surfaces need: their standing at the gym they picked.
104
133
  * `null` when they have never requested one.
@@ -1 +1 @@
1
- {"version":3,"file":"membership-request.d.ts","sourceRoot":"","sources":["../src/membership-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,eAAO,MAAM,6BAA6B;;;;EAA8C,CAAC;AACzF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;iBAYlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;iBAKtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAIhF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC;;iBAE5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,0EAA0E;AAC1E,eAAO,MAAM,iCAAiC;;iBAE5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,eAAO,MAAM,4BAA4B;;;;;;;;iBAEvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF;;;GAGG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;kBAEzB,CAAC;AACd,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
1
+ {"version":3,"file":"membership-request.d.ts","sourceRoot":"","sources":["../src/membership-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,eAAO,MAAM,6BAA6B;;;;EAA8C,CAAC;AACzF,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAIpF,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;iBAYlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;iBAKtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAIhF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC;;iBAE5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,0EAA0E;AAC1E,eAAO,MAAM,iCAAiC;;iBAE5C,CAAC;AACH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAE5F,eAAO,MAAM,4BAA4B;;;;;;;;iBAEvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;iBAI9C,CAAC;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC;AAEhG;;;GAGG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;kBAEzB,CAAC;AACd,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -75,6 +75,31 @@ export const decideMembershipRequestBodySchema = z.object({
75
75
  export const membershipRequestQuerySchema = paginationSchema.extend({
76
76
  status: membershipRequestStatusSchema.optional(),
77
77
  });
78
+ /**
79
+ * Where ONE request stands — the answer a notification card redraws itself from.
80
+ *
81
+ * ── Why this exists as its own reading ──────────────────────────────────────
82
+ * A join request can be answered from the notification feed, from the requests
83
+ * inbox, or by a different member of staff on a different phone. Whichever
84
+ * happens, every OTHER surface showing that request is now stale — and the one
85
+ * showing Accept / Decline buttons is the dangerous kind of stale, because the
86
+ * buttons still look live and the second tap fails.
87
+ *
88
+ * A card that tracks its own decision in component state only knows about taps
89
+ * made in THAT session: reload the feed, or navigate away and back, and the
90
+ * buttons return on a request that was settled days ago. This is the
91
+ * server-side truth that stops that, so a settled request reads as settled
92
+ * everywhere, forever, no matter who settled it.
93
+ *
94
+ * Deliberately just the decision. The notification already carries the member's
95
+ * name and avatar, so this must not become a second, thinner way to read a
96
+ * person's details.
97
+ */
98
+ export const membershipRequestStatusResultSchema = z.object({
99
+ status: membershipRequestStatusSchema,
100
+ /** When it was answered. Null while still pending. */
101
+ decidedAt: isoDateTimeSchema.nullable(),
102
+ });
78
103
  /**
79
104
  * What the member's own surfaces need: their standing at the gym they picked.
80
105
  * `null` when they have never requested one.
@@ -1 +1 @@
1
- {"version":3,"file":"membership-request.js","sourceRoot":"","sources":["../src/membership-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAEhF,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;AAGzF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,cAAc;IAClB,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,cAAc;IACxB,MAAM,EAAE,6BAA6B;IACrC,WAAW,EAAE,iBAAiB;IAC9B,0BAA0B;IAC1B,SAAS,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACvC,mEAAmE;IACnE,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,0DAA0D;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,uBAAuB,CAAC,MAAM,CAAC;IACxE,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;CACjC,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxD,OAAO,EAAE,gBAAgB;CAC1B,CAAC,CAAC;AAGH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxD,IAAI,EAAE,qBAAqB,CAAC,GAAG,CAAC;CACjC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,4BAA4B,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAClE,MAAM,EAAE,6BAA6B,CAAC,QAAQ,EAAE;CACjD,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,uBAAuB;KAC7D,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;KACvD,QAAQ,EAAE,CAAC","sourcesContent":["/**\n * gymmonk-schema — Membership join requests\n * ==========================================\n * A member asking to join a gym, and the gym's decision on it.\n *\n * WHY THIS EXISTS\n * A member picks a gym during onboarding, but picking is a CLAIM, not\n * membership. Until someone at the gym (owner, manager or front desk) accepts,\n * the person is not a member of that center and cannot check in. Without this\n * step anyone could self-serve into any gym's roster and start marking\n * attendance there.\n *\n * LIFECYCLE\n * member selects gym → `pending`\n * gym accepts → `approved` (member may now check in)\n * gym declines → `rejected` (member may request again, or elsewhere)\n * member cancels their plan → the approval is spent; a new request is needed\n *\n * A gym-CREATED member (the invite/claim path) never produces a request: the\n * gym made the record, so it has already decided.\n *\n * This is a LOG, not a flag: one row per request, so a cancel-and-rejoin cycle\n * leaves a trail and a rejection is not silently overwritten by the next\n * attempt. The member's CURRENT standing is the latest row for that center.\n *\n * @module gymmonk-schema/membership-request\n */\n\nimport { z } from 'zod';\nimport { codeLookupSchema } from './codes.js';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n} from './common.js';\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const membershipRequestStatusSchema = z.enum(['pending', 'approved', 'rejected']);\nexport type MembershipRequestStatus = z.infer<typeof membershipRequestStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const membershipRequestSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema,\n centerId: objectIdSchema,\n status: membershipRequestStatusSchema,\n requestedAt: isoDateTimeSchema,\n /** Null while pending. */\n decidedAt: isoDateTimeSchema.nullable(),\n /** The staff user who accepted or declined. Null while pending. */\n decidedBy: objectIdSchema.nullable(),\n /** Optional reason shown to the member on a rejection. */\n note: z.string().nullable(),\n});\nexport type MembershipRequest = z.infer<typeof membershipRequestSchema>;\n\n/**\n * A row in the gym's requests inbox: the request plus enough of the person to\n * decide without opening each one.\n */\nexport const membershipRequestItemSchema = membershipRequestSchema.extend({\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n});\nexport type MembershipRequestItem = z.infer<typeof membershipRequestItemSchema>;\n\n// ─── Requests ────────────────────────────────────────────────────────────────\n\n/**\n * The member asks to join. Identified by the gym's PUBLIC code (`IRN-1-7K2M`)\n * — that is what the member typed or picked during onboarding, and it keeps the\n * center's Mongo id out of a member-facing request.\n *\n * `codeLookupSchema` rather than `centerCodeSchema`: this normalises what the\n * member typed and lets the LOOKUP answer, so a code for a gym that has not\n * registered is refused with \"no gym found with that id\" instead of a regex\n * telling them they mistyped when they did not.\n */\nexport const createMembershipRequestBodySchema = z.object({\n gymCode: codeLookupSchema,\n});\nexport type CreateMembershipRequestBody = z.infer<typeof createMembershipRequestBodySchema>;\n\n/** Accept or decline. The note is surfaced to the member on a decline. */\nexport const decideMembershipRequestBodySchema = z.object({\n note: optionalTrimmedString(300),\n});\nexport type DecideMembershipRequestBody = z.infer<typeof decideMembershipRequestBodySchema>;\n\nexport const membershipRequestQuerySchema = paginationSchema.extend({\n status: membershipRequestStatusSchema.optional(),\n});\nexport type MembershipRequestQuery = z.infer<typeof membershipRequestQuerySchema>;\n\n/**\n * What the member's own surfaces need: their standing at the gym they picked.\n * `null` when they have never requested one.\n */\nexport const myMembershipRequestSchema = membershipRequestSchema\n .extend({ centerName: z.string(), gymCode: z.string() })\n .nullable();\nexport type MyMembershipRequest = z.infer<typeof myMembershipRequestSchema>;\n"]}
1
+ {"version":3,"file":"membership-request.js","sourceRoot":"","sources":["../src/membership-request.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,aAAa,CAAC;AAErB,gFAAgF;AAEhF,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;AAGzF,gFAAgF;AAEhF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,EAAE,EAAE,cAAc;IAClB,MAAM,EAAE,cAAc;IACtB,QAAQ,EAAE,cAAc;IACxB,MAAM,EAAE,6BAA6B;IACrC,WAAW,EAAE,iBAAiB;IAC9B,0BAA0B;IAC1B,SAAS,EAAE,iBAAiB,CAAC,QAAQ,EAAE;IACvC,mEAAmE;IACnE,SAAS,EAAE,cAAc,CAAC,QAAQ,EAAE;IACpC,0DAA0D;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,uBAAuB,CAAC,MAAM,CAAC;IACxE,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;CACjC,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxD,OAAO,EAAE,gBAAgB;CAC1B,CAAC,CAAC;AAGH,0EAA0E;AAC1E,MAAM,CAAC,MAAM,iCAAiC,GAAG,CAAC,CAAC,MAAM,CAAC;IACxD,IAAI,EAAE,qBAAqB,CAAC,GAAG,CAAC;CACjC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,4BAA4B,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAClE,MAAM,EAAE,6BAA6B,CAAC,QAAQ,EAAE;CACjD,CAAC,CAAC;AAGH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1D,MAAM,EAAE,6BAA6B;IACrC,sDAAsD;IACtD,SAAS,EAAE,iBAAiB,CAAC,QAAQ,EAAE;CACxC,CAAC,CAAC;AAGH;;;GAGG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,uBAAuB;KAC7D,MAAM,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC;KACvD,QAAQ,EAAE,CAAC","sourcesContent":["/**\n * gymmonk-schema — Membership join requests\n * ==========================================\n * A member asking to join a gym, and the gym's decision on it.\n *\n * WHY THIS EXISTS\n * A member picks a gym during onboarding, but picking is a CLAIM, not\n * membership. Until someone at the gym (owner, manager or front desk) accepts,\n * the person is not a member of that center and cannot check in. Without this\n * step anyone could self-serve into any gym's roster and start marking\n * attendance there.\n *\n * LIFECYCLE\n * member selects gym → `pending`\n * gym accepts → `approved` (member may now check in)\n * gym declines → `rejected` (member may request again, or elsewhere)\n * member cancels their plan → the approval is spent; a new request is needed\n *\n * A gym-CREATED member (the invite/claim path) never produces a request: the\n * gym made the record, so it has already decided.\n *\n * This is a LOG, not a flag: one row per request, so a cancel-and-rejoin cycle\n * leaves a trail and a rejection is not silently overwritten by the next\n * attempt. The member's CURRENT standing is the latest row for that center.\n *\n * @module gymmonk-schema/membership-request\n */\n\nimport { z } from 'zod';\nimport { codeLookupSchema } from './codes.js';\nimport {\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n} from './common.js';\n\n// ─── Status ──────────────────────────────────────────────────────────────────\n\nexport const membershipRequestStatusSchema = z.enum(['pending', 'approved', 'rejected']);\nexport type MembershipRequestStatus = z.infer<typeof membershipRequestStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const membershipRequestSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema,\n centerId: objectIdSchema,\n status: membershipRequestStatusSchema,\n requestedAt: isoDateTimeSchema,\n /** Null while pending. */\n decidedAt: isoDateTimeSchema.nullable(),\n /** The staff user who accepted or declined. Null while pending. */\n decidedBy: objectIdSchema.nullable(),\n /** Optional reason shown to the member on a rejection. */\n note: z.string().nullable(),\n});\nexport type MembershipRequest = z.infer<typeof membershipRequestSchema>;\n\n/**\n * A row in the gym's requests inbox: the request plus enough of the person to\n * decide without opening each one.\n */\nexport const membershipRequestItemSchema = membershipRequestSchema.extend({\n name: z.string(),\n email: z.string(),\n phone: z.string().nullable(),\n avatarUrl: z.string().nullable(),\n});\nexport type MembershipRequestItem = z.infer<typeof membershipRequestItemSchema>;\n\n// ─── Requests ────────────────────────────────────────────────────────────────\n\n/**\n * The member asks to join. Identified by the gym's PUBLIC code (`IRN-1-7K2M`)\n * — that is what the member typed or picked during onboarding, and it keeps the\n * center's Mongo id out of a member-facing request.\n *\n * `codeLookupSchema` rather than `centerCodeSchema`: this normalises what the\n * member typed and lets the LOOKUP answer, so a code for a gym that has not\n * registered is refused with \"no gym found with that id\" instead of a regex\n * telling them they mistyped when they did not.\n */\nexport const createMembershipRequestBodySchema = z.object({\n gymCode: codeLookupSchema,\n});\nexport type CreateMembershipRequestBody = z.infer<typeof createMembershipRequestBodySchema>;\n\n/** Accept or decline. The note is surfaced to the member on a decline. */\nexport const decideMembershipRequestBodySchema = z.object({\n note: optionalTrimmedString(300),\n});\nexport type DecideMembershipRequestBody = z.infer<typeof decideMembershipRequestBodySchema>;\n\nexport const membershipRequestQuerySchema = paginationSchema.extend({\n status: membershipRequestStatusSchema.optional(),\n});\nexport type MembershipRequestQuery = z.infer<typeof membershipRequestQuerySchema>;\n\n/**\n * Where ONE request stands — the answer a notification card redraws itself from.\n *\n * ── Why this exists as its own reading ──────────────────────────────────────\n * A join request can be answered from the notification feed, from the requests\n * inbox, or by a different member of staff on a different phone. Whichever\n * happens, every OTHER surface showing that request is now stale — and the one\n * showing Accept / Decline buttons is the dangerous kind of stale, because the\n * buttons still look live and the second tap fails.\n *\n * A card that tracks its own decision in component state only knows about taps\n * made in THAT session: reload the feed, or navigate away and back, and the\n * buttons return on a request that was settled days ago. This is the\n * server-side truth that stops that, so a settled request reads as settled\n * everywhere, forever, no matter who settled it.\n *\n * Deliberately just the decision. The notification already carries the member's\n * name and avatar, so this must not become a second, thinner way to read a\n * person's details.\n */\nexport const membershipRequestStatusResultSchema = z.object({\n status: membershipRequestStatusSchema,\n /** When it was answered. Null while still pending. */\n decidedAt: isoDateTimeSchema.nullable(),\n});\nexport type MembershipRequestStatusResult = z.infer<typeof membershipRequestStatusResultSchema>;\n\n/**\n * What the member's own surfaces need: their standing at the gym they picked.\n * `null` when they have never requested one.\n */\nexport const myMembershipRequestSchema = membershipRequestSchema\n .extend({ centerName: z.string(), gymCode: z.string() })\n .nullable();\nexport type MyMembershipRequest = z.infer<typeof myMembershipRequestSchema>;\n"]}
@@ -475,10 +475,32 @@ export declare const ERROR_MESSAGE: {
475
475
  /** Searching by name needs somewhere to look — see `gymSearchQuerySchema`. */
476
476
  readonly gymSearchStateRequired: "Pick a state to search gyms by name.";
477
477
  readonly gymSearchNoResults: "No gyms matched. Try a different area, or enter the gym id.";
478
- readonly cameraDenied: "Camera access is blocked. Allow it in your browser settings to scan.";
479
- readonly cameraUnavailable: "No camera found on this device. Enter the code printed under the QR.";
478
+ /**
479
+ * Camera (QR scanning)
480
+ *
481
+ * ── THESE USED TO POINT AT A BOX THAT DOES NOT EXIST ──────────────────────
482
+ * Two of them ended "Enter the code printed under the QR", and both halves of
483
+ * that sentence were wrong: the scanner has no field to type a code into, and
484
+ * the poster does not print the check-in code at all — it prints the gym's
485
+ * PUBLIC join code, which cannot check anybody in. So a member whose camera
486
+ * failed was sent looking for a box that is not there, to type a number that
487
+ * is not printed.
488
+ *
489
+ * They now point at the thing that IS there: the check-in button under the
490
+ * viewfinder, which needs no code at all.
491
+ */
492
+ readonly cameraDenied: "Camera access is blocked. Allow it in your browser settings, or check in without scanning.";
493
+ readonly cameraUnavailable: "No camera found on this device. Check in without scanning instead.";
480
494
  readonly cameraInsecure: "Scanning needs a secure (https) connection.";
481
- readonly cameraFailed: "Could not start the camera. Enter the code printed under the QR.";
495
+ readonly cameraFailed: "Could not start the camera. Check in without scanning instead.";
496
+ /**
497
+ * The member is not attached to a gym, on the codeless path.
498
+ *
499
+ * Its own sentence because that path reached for `qrUnknown` before it had
500
+ * one, and that string opens "This QR is no longer valid" — advice about a
501
+ * poster, given to somebody who never scanned one.
502
+ */
503
+ readonly gymUnavailable: "You are not checked in to a gym yet. Join one from the Me tab first.";
482
504
  readonly feedbackRatingRequired: "Choose a rating to submit your feedback";
483
505
  readonly complaintIssueRequired: "Choose the issue you are reporting";
484
506
  readonly mpinIncorrect: "Incorrect PIN.";
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;IAsBrB;;;;;;;;;;;;;OAaG;;;CAKK,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAIrE,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;IAU1B,sEAAsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8CtE;;;;OAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH6C,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,eAAe,CAAC;AAE7D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB;iCACV,MAAM,KAAG,aAAa;gCAIvB,MAAM,KAAG,aAAa;sCAIhB,MAAM,KAAG,aAAa;uCAIrB,MAAM,KAAG,aAAa;uCAGtB,MAAM,KAAG,aAAa;uCAItB,MAAM,KAAG,aAAa;CAIxC,CAAC;AAIX;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;IAiBxB;;;;;OAKG;;IAGH;;;;;;;OAOG;;IAGH;;;;;;OAMG;;IAGH,4EAA4E;;IAE5E,+EAA+E;;IAE/E,gFAAgF;;IAGhF,iDAAiD;;;IAKjD,kDAAkD;;IAElD;;;;OAIG;;IAEH;;;;OAIG;;IAEH,qEAAqE;;IAErE,2EAA2E;;IAE3E,wEAAwE;;;;;;;IASxE,gEAAgE;;;;;;;;;;;IAehE,2EAA2E;;;IAI3E;;;OAGG;;;IAIH;;;;OAIG;;IAGH;;;;OAIG;;IAEH;;;;;OAKG;;IAGH;;;OAGG;;IAGH,mEAAmE;;;;;IAKnE,8EAA8E;;;;;;;;;;;;;IAgB9E;;;OAGG;;;;IAMH,wEAAwE;;IAExE,uEAAuE;;IAEvE,yEAAyE;;IAEzE,8EAA8E;;IAE9E,0DAA0D;;;IAG1D;;;;OAIG;;IAIH,4EAA4E;;IAG5E,6DAA6D;;IAE7D,kDAAkD;;IAElD,+DAA+D;;IAE/D,sEAAsE;;;IAItE,kEAAkE;;;;IAIlE,uEAAuE;;IAEvE,8EAA8E;;;;;;;;;;;;;;;;;CAyBrC,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD,iDAAiD;AACjD,eAAO,MAAM,iBAAiB;8BACX,MAAM,KAAG,MAAM;IAChC;;;;;;;;OAQG;gDACgC,MAAM,KAAG,MAAM;IAElD,2DAA2D;sCAClC,MAAM,KAAG,MAAM;CAEhC,CAAC"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;IAsBrB;;;;;;;;;;;;;OAaG;;;CAKK,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAIrE,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;;;IAU1B,sEAAsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA8CtE;;;;OAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkH6C,CAAC;AAEnD,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,eAAe,CAAC;AAE7D,0DAA0D;AAC1D,eAAO,MAAM,mBAAmB;iCACV,MAAM,KAAG,aAAa;gCAIvB,MAAM,KAAG,aAAa;sCAIhB,MAAM,KAAG,aAAa;uCAIrB,MAAM,KAAG,aAAa;uCAGtB,MAAM,KAAG,aAAa;uCAItB,MAAM,KAAG,aAAa;CAIxC,CAAC;AAIX;;;;GAIG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;IAiBxB;;;;;OAKG;;IAGH;;;;;;;OAOG;;IAGH;;;;;;OAMG;;IAGH,4EAA4E;;IAE5E,+EAA+E;;IAE/E,gFAAgF;;IAGhF,iDAAiD;;;IAKjD,kDAAkD;;IAElD;;;;OAIG;;IAEH;;;;OAIG;;IAEH,qEAAqE;;IAErE,2EAA2E;;IAE3E,wEAAwE;;;;;;;IASxE,gEAAgE;;;;;;;;;;;IAehE,2EAA2E;;;IAI3E;;;OAGG;;;IAIH;;;;OAIG;;IAGH;;;;OAIG;;IAEH;;;;;OAKG;;IAGH;;;OAGG;;IAGH,mEAAmE;;;;;IAKnE,8EAA8E;;;;;;;;;;;;;IAgB9E;;;OAGG;;;;IAMH,wEAAwE;;IAExE,uEAAuE;;IAEvE,yEAAyE;;IAEzE,8EAA8E;;IAE9E,0DAA0D;;;IAG1D;;;;OAIG;;IAIH,4EAA4E;;IAG5E,6DAA6D;;IAE7D,kDAAkD;;IAElD,+DAA+D;;IAE/D,sEAAsE;;;IAItE,kEAAkE;;;;IAIlE,uEAAuE;;IAEvE,8EAA8E;;;IAI9E;;;;;;;;;;;;;OAaG;;;;;IAMH;;;;;;OAMG;;;;;;;;;;;;CAkBsC,CAAC;AAE5C,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,aAAa,CAAC;AAEzD,iDAAiD;AACjD,eAAO,MAAM,iBAAiB;8BACX,MAAM,KAAG,MAAM;IAChC;;;;;;;;OAQG;gDACgC,MAAM,KAAG,MAAM;IAElD,2DAA2D;sCAClC,MAAM,KAAG,MAAM;CAEhC,CAAC"}
package/dist/messages.js CHANGED
@@ -442,11 +442,32 @@ export const ERROR_MESSAGE = {
442
442
  /** Searching by name needs somewhere to look — see `gymSearchQuerySchema`. */
443
443
  gymSearchStateRequired: 'Pick a state to search gyms by name.',
444
444
  gymSearchNoResults: 'No gyms matched. Try a different area, or enter the gym id.',
445
- // Camera (QR scanning)
446
- cameraDenied: 'Camera access is blocked. Allow it in your browser settings to scan.',
447
- cameraUnavailable: 'No camera found on this device. Enter the code printed under the QR.',
445
+ /**
446
+ * Camera (QR scanning)
447
+ *
448
+ * ── THESE USED TO POINT AT A BOX THAT DOES NOT EXIST ──────────────────────
449
+ * Two of them ended "Enter the code printed under the QR", and both halves of
450
+ * that sentence were wrong: the scanner has no field to type a code into, and
451
+ * the poster does not print the check-in code at all — it prints the gym's
452
+ * PUBLIC join code, which cannot check anybody in. So a member whose camera
453
+ * failed was sent looking for a box that is not there, to type a number that
454
+ * is not printed.
455
+ *
456
+ * They now point at the thing that IS there: the check-in button under the
457
+ * viewfinder, which needs no code at all.
458
+ */
459
+ cameraDenied: 'Camera access is blocked. Allow it in your browser settings, or check in without scanning.',
460
+ cameraUnavailable: 'No camera found on this device. Check in without scanning instead.',
448
461
  cameraInsecure: 'Scanning needs a secure (https) connection.',
449
- cameraFailed: 'Could not start the camera. Enter the code printed under the QR.',
462
+ cameraFailed: 'Could not start the camera. Check in without scanning instead.',
463
+ /**
464
+ * The member is not attached to a gym, on the codeless path.
465
+ *
466
+ * Its own sentence because that path reached for `qrUnknown` before it had
467
+ * one, and that string opens "This QR is no longer valid" — advice about a
468
+ * poster, given to somebody who never scanned one.
469
+ */
470
+ gymUnavailable: 'You are not checked in to a gym yet. Join one from the Me tab first.',
450
471
  // Engagement
451
472
  feedbackRatingRequired: 'Choose a rating to submit your feedback',
452
473
  complaintIssueRequired: 'Choose the issue you are reporting',
@@ -1 +1 @@
1
- {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — API result messages\n * ====================================\n * Single source of truth for every user-facing SUCCESS and ERROR message the\n * product shows, plus the API error codes both sides key off.\n *\n * Why it lives here: the schema package is already the SSOT for data shapes,\n * types and validation. Result copy is part of that contract — the backend\n * raises a message, the client shows it, and neither should invent its own\n * wording. Both repos import from here, so copy changes land in one place and\n * can never drift between server and client.\n *\n * Usage:\n * backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`\n * frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`\n *\n * @module gymmonk-schema/messages\n */\n\n// ─── API error codes ─────────────────────────────────────────────────────────\n\n/**\n * Machine-readable error codes returned in the API envelope\n * (`{ success: false, error: { code, message } }`). The client can branch on a\n * code; the message is what a human reads.\n */\nexport const ERROR_CODE = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n NOT_FOUND: 'NOT_FOUND',\n\n // Auth\n INVALID_TOKEN: 'INVALID_TOKEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n SESSION_INVALIDATED: 'SESSION_INVALIDATED',\n\n // User\n USER_NOT_FOUND: 'USER_NOT_FOUND',\n USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',\n INVALID_ROLE: 'INVALID_ROLE',\n\n // Rate limiting\n RATE_LIMITED: 'RATE_LIMITED',\n RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',\n\n /**\n * The caller MAY do this — their subscription simply does not stretch to it.\n *\n * Deliberately NOT `FORBIDDEN`, and the distinction is load-bearing on the\n * client. A role denial means \"this is not yours\"; the answer is to go away.\n * A plan denial means \"this costs money\"; the answer is an upgrade prompt with\n * the price on it. A client that cannot tell them apart either shows an\n * upgrade sheet to a trainer who will never be allowed to buy one, or shows a\n * bare 403 to an owner who would happily have paid.\n *\n * Always accompanied by `details`: `{ feature, currentPlan, requiredPlan,\n * current?, limit? }` — enough for the prompt to name the feature, the tier\n * that unlocks it, and how full the current allowance is.\n */\n PLAN_LIMIT_EXCEEDED: 'PLAN_LIMIT_EXCEEDED',\n\n // Upstream / dependency unavailable\n SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];\n\n// ─── Message shape ───────────────────────────────────────────────────────────\n\n/** A result message: a headline and an optional quieter second line. */\nexport interface ResultMessage {\n title: string;\n description?: string;\n}\n\n// ─── Success messages ────────────────────────────────────────────────────────\n\n/**\n * Confirmation shown after a write succeeds, keyed by action. Interpolating\n * entries are functions so the caller supplies the value without re-wording.\n */\nexport const SUCCESS_MESSAGE = {\n // Onboarding\n memberOnboardingComplete: {\n title: \"You're all set\",\n description: 'Welcome to GymMonk.',\n },\n ownerOnboardingComplete: {\n title: 'Your gym is ready',\n description: 'Welcome to GymMonk.',\n },\n /** Each wizard step is persisted on its own, so each one confirms. */\n onboardingStepSaved: { title: 'Saved' },\n\n // Identity & profile\n profileUpdated: { title: 'Profile updated' },\n organisationUpdated: { title: 'Organisation updated' },\n\n // Gym structure\n centerAdded: { title: 'Center added' },\n centerUpdated: { title: 'Center updated' },\n membershipPlanCreated: { title: 'Membership plan created' },\n membershipPlanUpdated: { title: 'Membership plan updated' },\n equipmentSaved: { title: 'Equipment updated' },\n\n // Membership (member-side)\n membershipUpdated: { title: 'Membership updated' },\n membershipRenewed: { title: 'Membership renewed' },\n membershipCancelled: { title: 'Membership cancelled' },\n membershipPauseRequested: {\n title: 'Pause request sent',\n description: 'Your gym will confirm the pause.',\n },\n\n // Owner SaaS subscription\n subscriptionUpdated: { title: 'Subscription updated' },\n\n // Training\n workoutPlanCreated: { title: 'Workout plan created' },\n workoutPlanUpdated: { title: 'Workout plan saved' },\n workoutPlanDeleted: {\n title: 'Workout plan deleted',\n description: 'Members on this plan no longer have one.',\n },\n workoutCreated: { title: 'Workout created' },\n workoutUpdated: { title: 'Workout saved' },\n workoutDeleted: { title: 'Workout removed' },\n workoutPlanAssigned: { title: 'Workout plan assigned' },\n\n // Roster (owner console)\n attendanceMarked: { title: 'Attendance saved' },\n memberMarkedInactive: {\n title: 'Member marked inactive',\n description: 'They can no longer check in.',\n },\n memberMarkedActive: { title: 'Member reactivated' },\n memberDeleted: { title: 'Member removed' },\n /**\n * Says what actually happened. Assigning a plan no longer starts it —\n * nothing does until the payment is marked done — so a bare \"Plan\n * updated\" would send the owner away believing the member could train.\n */\n memberPlanAssigned: {\n title: 'Plan assigned',\n description: 'It starts once you mark the payment done.',\n },\n memberMembershipPaused: { title: 'Membership paused' },\n memberMembershipCancelled: { title: 'Membership cancelled' },\n paymentCollected: { title: 'Payment recorded' },\n\n // Sessions\n checkedIn: { title: 'Checked in', description: 'Your session has started.' },\n checkedOut: { title: 'Checked out', description: 'Session saved.' },\n checkInQrRotated: {\n title: 'New QR generated',\n description: 'Print it and replace the old poster.',\n },\n\n // Membership join requests\n membershipRequestSent: {\n title: 'Request sent',\n description: 'The gym will review it shortly.',\n },\n membershipRequestApproved: {\n title: 'Member approved',\n description: 'They can now check in at your gym.',\n },\n membershipRequestRejected: {\n title: 'Request declined',\n description: 'The member has been told.',\n },\n\n // Membership payment confirmation\n membershipRequested: {\n title: 'Plan requested',\n description: 'It starts once your gym confirms the payment.',\n },\n membershipRequestWithdrawn: {\n title: 'Request withdrawn',\n description: 'Pick another plan whenever you are ready.',\n },\n membershipPaymentConfirmed: {\n title: 'Payment confirmed',\n description: 'The plan is now active for this member.',\n },\n membershipPaymentRejected: {\n title: 'Payment turned down',\n description: 'The member has been told and can send it again.',\n },\n\n // Gym registration + platform review\n gymRegistered: {\n title: 'Gym submitted',\n description: 'Our team will review it and let you know.',\n },\n gymApproved: {\n title: 'Gym approved',\n description: 'It is now listed for members to find.',\n },\n gymRejected: {\n title: 'Gym rejected',\n description: 'The owner has been told.',\n },\n\n // Engagement\n feedbackSubmitted: {\n title: 'Feedback sent',\n description: 'Thank you for helping us improve.',\n },\n complaintSubmitted: {\n title: 'Complaint submitted',\n description: 'Your gym will look into it.',\n },\n announcementSent: { title: 'Announcement sent' },\n announcementScheduled: { title: 'Announcement scheduled' },\n announcementDraftSaved: { title: 'Draft saved' },\n announcementDeleted: { title: 'Announcement deleted' },\n announcementCancelled: {\n title: 'Announcement cancelled',\n description: 'It will not be sent.',\n },\n\n // Notification feed\n notificationHidden: { title: 'Notification hidden' },\n notificationRestored: { title: 'Notification restored' },\n allNotificationsRead: { title: 'All caught up' },\n\n // Security (role-switch PIN)\n mpinSet: { title: 'PIN set' },\n mpinReset: { title: 'Your PIN has been reset.' },\n mpinResetLinkSent: {\n title: \"We've emailed you a reset link\",\n description: 'Check your inbox.',\n },\n\n // ── Platform console (superadmin / platform manager) ────────────────────────\n platformRolesUpdated: { title: 'Platform roles updated' },\n platformUserSuspended: {\n title: 'Account suspended',\n description: 'They can no longer sign in.',\n },\n platformUserReinstated: { title: 'Account reinstated' },\n platformUserDeleted: {\n title: 'Account deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformUserRestored: { title: 'Account restored' },\n platformGymDeleted: {\n title: 'Gym deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformGymRestored: {\n title: 'Gym restored',\n description: 'It is back in the directory and its owner has the console again.',\n },\n} as const satisfies Record<string, ResultMessage>;\n\nexport type SuccessMessageKey = keyof typeof SUCCESS_MESSAGE;\n\n/** Success messages that name the thing they acted on. */\nexport const SUCCESS_MESSAGE_FOR = {\n memberAdded: (name: string): ResultMessage => ({\n title: 'Member added',\n description: `Share the invite with ${name}.`,\n }),\n staffAdded: (name: string): ResultMessage => ({\n title: 'Staff added',\n description: `Share the invite with ${name}.`,\n }),\n inviteClaimed: (gymName: string): ResultMessage => ({\n title: \"You're all set\",\n description: `Your account is linked to ${gymName}.`,\n }),\n roleSwitched: (roleLabel: string): ResultMessage => ({\n title: `Switched to ${roleLabel}`,\n }),\n gymApprovedFor: (gymName: string): ResultMessage => ({\n title: 'Gym approved',\n description: `${gymName} is now live on GymMonk.`,\n }),\n gymRejectedFor: (gymName: string): ResultMessage => ({\n title: 'Gym rejected',\n description: `The owner of ${gymName} has been told why.`,\n }),\n} as const;\n\n// ─── Error messages ──────────────────────────────────────────────────────────\n\n/**\n * Failure copy. Transport entries describe a request that never reached the\n * API; the rest are domain rules the backend enforces and the client can also\n * check up-front — same wording either way.\n */\nexport const ERROR_MESSAGE = {\n // Transport / generic\n generic: 'Something went wrong. Please try again.',\n network: 'Network error. Check your connection and try again.',\n timeout: 'That took too long. Please try again.',\n sessionExpired: 'Your session expired. Please sign in again.',\n\n // Client-side validation\n formInvalid: 'Please check the highlighted fields.',\n\n // Onboarding\n roleRequired: 'Choose a role to continue',\n ownerOnboardingFailed: 'We could not create your gym. Please try again.',\n memberOnboardingFailed: 'We could not save your details. Please try again.',\n\n // ── Gym registration (\"list your gym\") ─────────────────────────────────────\n gymRegistrationFailed: 'We could not register your gym. Please try again.',\n /**\n * The wizard could not read what the owner had already saved.\n *\n * It refuses to open on a blank form rather than risk the next Continue\n * overwriting a real draft, so this is the whole screen, not a toast.\n */\n gymRegistrationDraftLoadFailed:\n 'We could not open your saved gym details. Check your connection and try again.',\n /**\n * The step the wizard tried to save is implausibly large.\n *\n * A guard against a client sending something absurd rather than a limit the\n * real form approaches, but it is raised by the SERVER and shown verbatim in\n * the step-save toast — so the wording lives here with the rest of the copy,\n * not as a hand-written string at the throw site.\n */\n gymRegistrationDraftTooLarge:\n 'Those gym details are too large to save. Please shorten the descriptions and try again.',\n /**\n * \"Near me\" found nothing inside the radius.\n *\n * Deliberately not the same as `gymSearchNoResults`: this member searched\n * nowhere in particular, so telling them to refine their search would be\n * advice they cannot act on. What helps is a different place or a gym id.\n */\n gymSearchNoneNearby:\n 'No gyms within 50 km of you yet. Try a different location, or enter a gym id if you have one.',\n /** One organisation per account — registering a second is not supported. */\n gymAlreadyRegistered: 'You have already registered a gym on this account.',\n /** Registering a gym is something a MEMBER does, so the profile must exist. */\n gymRegistrationNeedsProfile: 'Finish setting up your profile before registering a gym.',\n /** Every owner action other than editing their own details, before approval. */\n gymAwaitingApproval:\n 'Your gym is waiting for approval. You can manage it once our team has reviewed it.',\n /** The reviewer turned the registration down. */\n gymRegistrationRejected: 'Your gym registration was not approved.',\n organisationNotFound: 'That gym could not be found.',\n\n // ── Platform console ────────────────────────────────────────────────────────\n /** The account is gone, or was already erased. */\n platformUserNotFound: 'That account could not be found.',\n /**\n * An admin acting on their OWN row. Suspending or deleting yourself takes away\n * the access needed to undo it, so the console refuses rather than letting one\n * mis-tap lock the platform's last superadmin out of it.\n */\n platformCannotActOnSelf: 'You cannot do that to your own account.',\n /**\n * Removing the last superadmin. `manage GymApproval` is held by nobody else,\n * so the gym review queue would become unreachable with no way back in short\n * of a database edit.\n */\n platformLastSuperadmin: 'The platform must keep at least one superadmin.',\n /** The typed confirmation did not match the record being deleted. */\n platformConfirmationMismatch: 'That did not match. Nothing was changed.',\n /** Two reviewers acting on the same registration; the second one loses. */\n organisationAlreadyDecided: 'Someone has already reviewed this gym.',\n /** A step failed to persist — the user stays put so nothing is lost. */\n onboardingStepSaveFailed: 'We could not save this step. Please try again.',\n\n // Gym structure prerequisites\n centerRequiredForPlan: 'Pick the center this plan belongs to.',\n planVariantRequired: 'Add at least one pricing variant before saving the plan.',\n planNameTaken: 'This center already has a plan with that name. Pick another name.',\n planVariantDurationRequired: 'Enter how long this variant runs for',\n planVariantPriceRequired: 'Enter the price for this variant',\n /** A \"discount\" that raises the price is a typo, every time. */\n planFinalPriceAboveOriginal: 'The final price cannot be more than the original price',\n centerAddressRequired: 'Add the center address before creating it.',\n noCenterForPlans: 'No gym center found. Add a center before creating plans.',\n noCenterForWorkouts: 'No gym center found. Add a center before creating workouts.',\n noCenterForAnnouncements: 'No gym center found. Add a center before sending announcements.',\n announcementAlreadySent: 'That announcement has already gone out, so it cannot be cancelled.',\n\n // Notification feed\n notificationHideFailed: 'Could not hide that notification.',\n notificationRestoreFailed: 'Could not restore that notification.',\n\n // Membership\n planUnavailable: 'That plan is not available right now.',\n membershipPlanRequired: 'Choose a membership plan to continue',\n /** The member was created, but the plan the owner picked did not stick. */\n initialPlanNotAssigned:\n 'Member added, but the plan could not be assigned. Assign it from their profile.',\n noPlansToAssign: 'No membership plans yet. Create one to assign it while adding a member.',\n /**\n * Not an error the member can act on by trying harder — it is the whole\n * point of the flow, so it reads as a status rather than a refusal.\n */\n membershipAwaitingPayment:\n 'Your gym is checking your payment. The plan starts as soon as they confirm it.',\n membershipNoPendingRequest: 'There is no plan waiting for confirmation.',\n /**\n * Pausing extends the term by the paused days, so pausing something that is\n * not running would push out a date nobody is training against — and pausing\n * an already-paused membership would extend it twice for one absence.\n */\n membershipNotPausable:\n 'Only a running membership can be paused. This one has already ended or is paused.',\n /**\n * The joining date records something that has already happened, so a future\n * one is not a preference to honour — it would date the member's whole\n * billing cycle from a day that has not arrived.\n */\n joiningDateInFuture: 'A joining date cannot be in the future.',\n /**\n * The term moved between the screen being drawn and the button being\n * pressed, which in practice means a second manager confirmed one first.\n * Says which month it now covers rather than a bare \"try again\", because the\n * owner's next question is always \"then what am I paying for?\".\n */\n membershipTermMoved:\n 'Someone just confirmed a payment for this member, so the period has moved on. Reopen their profile to see the current one.',\n /**\n * The owner reached for the roster toggle on a plan that has not been paid\n * for. Points at the action that WOULD start it rather than just refusing.\n */\n membershipPendingUseConfirm:\n 'This plan is waiting for payment confirmation. Mark the payment done to start it.',\n /** Two managers opening the same request; the second one loses. */\n membershipAlreadyDecided: 'Someone has already answered this payment.',\n membershipConfirmFailed: 'Could not confirm that payment. Please try again.',\n membershipRejectFailed: 'Could not turn down that payment. Please try again.',\n membershipWithdrawFailed: 'Could not withdraw that request. Please try again.',\n /** The URL did not come from our own storage — a broken or hostile client. */\n paymentProofInvalid: 'That payment screenshot could not be attached. Upload it again.',\n\n // Training\n workoutPlanRequired: 'Choose a plan to assign',\n // Roster (owner console)\n attendanceMarkFailed: 'Could not save that attendance. Please try again.',\n memberStatusFailed: \"Could not change that member's status. Please try again.\",\n memberDeleteFailed: 'Could not remove that member. Please try again.',\n memberPlanAssignFailed: 'Could not update that plan. Please try again.',\n memberPauseFailed: 'Could not pause that membership. Please try again.',\n memberCancelFailed: 'Could not cancel that membership. Please try again.',\n paymentCollectFailed: 'Could not record that payment. Please try again.',\n paymentAmountRequired: 'Enter the amount collected',\n pauseDatesRequired: 'Pick both pause dates',\n pauseDatesOrder: 'The end date has to be after the start date',\n /**\n * Attendance, workout plans and payments are all keyed on the central-auth\n * user, which an invited record does not have until the person claims it.\n */\n memberNotClaimed: 'This member has not claimed their invite yet.',\n\n // Sessions\n checkInFailed: 'Could not check you in. Please try again.',\n checkOutFailed: 'Could not check you out. Please try again.',\n /** The camera decoded a QR, but it is not a gymmonk check-in poster. */\n qrNotRecognised: 'That is not a gym check-in QR. Point at the poster at the gym.',\n /** A valid gymmonk QR, but for a gym the member does not belong to. */\n qrWrongGym: 'That QR belongs to a different gym.',\n /** The scanned code matches no center — a retired or mistyped poster. */\n qrUnknown: 'This QR is no longer valid. Ask the gym for the current poster.',\n /** Checking out with a poster from somewhere other than where you started. */\n qrWrongGymForCheckOut: 'Scan the QR at the gym where you checked in.',\n /** Manual entry only accepts a full, well-formed code. */\n checkInCodeInvalid: 'Enter the 16-character code printed under the QR.',\n checkInQrRotateFailed: 'Could not generate a new QR. Please try again.',\n /**\n * Ticking an exercise off records it against a live session, so there has to\n * be one. Shown when a member opens an exercise outside a workout and taps\n * Mark As Done.\n */\n noSessionToTrack: 'Check in at your gym to track this workout.',\n\n // ── Membership join requests ───────────────────────────────────────────────\n /** Scanned a valid poster, but the gym has not accepted this member yet. */\n membershipNotApproved:\n 'Your gym has not accepted your join request yet. You can check in once they do.',\n /** Accepted, but with no live plan — check-in needs both. */\n membershipNotActive: 'Your membership plan is not active. Renew it to start checking in again.',\n /** A second request while one is already open. */\n membershipRequestPending: 'You already have a request waiting with this gym.',\n /** Requesting a gym they are already an accepted member of. */\n membershipRequestAlreadyMember: 'You are already a member of this gym.',\n /** v1 memberships are single-center — leave the current gym first. */\n membershipRequestAlreadyAtAnotherGym:\n 'You already belong to a gym. Cancel that membership before joining another.',\n membershipRequestNotFound: 'That join request no longer exists.',\n /** Two staff acting on the same request; the second one loses. */\n membershipRequestAlreadyDecided: 'Someone has already answered this request.',\n membershipRequestFailed: 'Could not send your request. Please try again.',\n gymCodeUnknown: 'No gym found with that id. Check it with your gym.',\n /** Actions that only make sense once a gym has taken the member on. */\n noGymJoined: 'Join a gym first. You can find one from your Me tab.',\n /** Searching by name needs somewhere to look — see `gymSearchQuerySchema`. */\n gymSearchStateRequired: 'Pick a state to search gyms by name.',\n gymSearchNoResults: 'No gyms matched. Try a different area, or enter the gym id.',\n\n // Camera (QR scanning)\n cameraDenied: 'Camera access is blocked. Allow it in your browser settings to scan.',\n cameraUnavailable: 'No camera found on this device. Enter the code printed under the QR.',\n cameraInsecure: 'Scanning needs a secure (https) connection.',\n cameraFailed: 'Could not start the camera. Enter the code printed under the QR.',\n\n // Engagement\n feedbackRatingRequired: 'Choose a rating to submit your feedback',\n complaintIssueRequired: 'Choose the issue you are reporting',\n\n // Security (role-switch PIN)\n mpinIncorrect: 'Incorrect PIN.',\n mpinVerifyFailed: \"Couldn't verify. Try again.\",\n mpinSetFailed: \"Couldn't set your PIN. Try again.\",\n mpinMismatch: \"Those didn't match. Try again.\",\n mpinResetLinkFailed: \"Couldn't send the reset link. Try again.\",\n mpinResetFailed: \"Couldn't reset your PIN. Try again.\",\n\n // Uploads\n imageUploadFailed: 'Could not upload that image. Please try again.',\n imageProcessFailed: 'Could not process that image. Please try again.',\n} as const satisfies Record<string, string>;\n\nexport type ErrorMessageKey = keyof typeof ERROR_MESSAGE;\n\n/** Error messages that name a limit or value. */\nexport const ERROR_MESSAGE_FOR = {\n maxPhotos: (max: number): string => `You can add up to ${max} photos`,\n /**\n * Refused at the door because the membership is paused and still inside its\n * window.\n *\n * Its own sentence because \"renew your plan\" is wrong advice here twice\n * over: nothing has lapsed, and paying again would not open the door a day\n * sooner. The resume date is the only thing this member wants to know, and\n * it is the reason this one takes a parameter at all.\n */\n membershipPausedUntil: (resumesOn: string): string =>\n `Your membership is paused until ${resumesOn}. You can check in again from that day.`,\n /** Role-switch PIN lockout, in whole minutes remaining. */\n mpinLockedOut: (minutes: number): string =>\n `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,\n} as const;\n"]}
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — API result messages\n * ====================================\n * Single source of truth for every user-facing SUCCESS and ERROR message the\n * product shows, plus the API error codes both sides key off.\n *\n * Why it lives here: the schema package is already the SSOT for data shapes,\n * types and validation. Result copy is part of that contract — the backend\n * raises a message, the client shows it, and neither should invent its own\n * wording. Both repos import from here, so copy changes land in one place and\n * can never drift between server and client.\n *\n * Usage:\n * backend → `throw new BadRequestError(ERROR_MESSAGE.centerAddressRequired)`\n * frontend → `showSuccess(...toast(SUCCESS_MESSAGE.profileUpdated))`\n *\n * @module gymmonk-schema/messages\n */\n\n// ─── API error codes ─────────────────────────────────────────────────────────\n\n/**\n * Machine-readable error codes returned in the API envelope\n * (`{ success: false, error: { code, message } }`). The client can branch on a\n * code; the message is what a human reads.\n */\nexport const ERROR_CODE = {\n // General\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n VALIDATION_ERROR: 'VALIDATION_ERROR',\n NOT_FOUND: 'NOT_FOUND',\n\n // Auth\n INVALID_TOKEN: 'INVALID_TOKEN',\n TOKEN_EXPIRED: 'TOKEN_EXPIRED',\n UNAUTHORIZED: 'UNAUTHORIZED',\n FORBIDDEN: 'FORBIDDEN',\n SESSION_INVALIDATED: 'SESSION_INVALIDATED',\n\n // User\n USER_NOT_FOUND: 'USER_NOT_FOUND',\n USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',\n INVALID_ROLE: 'INVALID_ROLE',\n\n // Rate limiting\n RATE_LIMITED: 'RATE_LIMITED',\n RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',\n\n /**\n * The caller MAY do this — their subscription simply does not stretch to it.\n *\n * Deliberately NOT `FORBIDDEN`, and the distinction is load-bearing on the\n * client. A role denial means \"this is not yours\"; the answer is to go away.\n * A plan denial means \"this costs money\"; the answer is an upgrade prompt with\n * the price on it. A client that cannot tell them apart either shows an\n * upgrade sheet to a trainer who will never be allowed to buy one, or shows a\n * bare 403 to an owner who would happily have paid.\n *\n * Always accompanied by `details`: `{ feature, currentPlan, requiredPlan,\n * current?, limit? }` — enough for the prompt to name the feature, the tier\n * that unlocks it, and how full the current allowance is.\n */\n PLAN_LIMIT_EXCEEDED: 'PLAN_LIMIT_EXCEEDED',\n\n // Upstream / dependency unavailable\n SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n} as const;\n\nexport type ErrorCode = (typeof ERROR_CODE)[keyof typeof ERROR_CODE];\n\n// ─── Message shape ───────────────────────────────────────────────────────────\n\n/** A result message: a headline and an optional quieter second line. */\nexport interface ResultMessage {\n title: string;\n description?: string;\n}\n\n// ─── Success messages ────────────────────────────────────────────────────────\n\n/**\n * Confirmation shown after a write succeeds, keyed by action. Interpolating\n * entries are functions so the caller supplies the value without re-wording.\n */\nexport const SUCCESS_MESSAGE = {\n // Onboarding\n memberOnboardingComplete: {\n title: \"You're all set\",\n description: 'Welcome to GymMonk.',\n },\n ownerOnboardingComplete: {\n title: 'Your gym is ready',\n description: 'Welcome to GymMonk.',\n },\n /** Each wizard step is persisted on its own, so each one confirms. */\n onboardingStepSaved: { title: 'Saved' },\n\n // Identity & profile\n profileUpdated: { title: 'Profile updated' },\n organisationUpdated: { title: 'Organisation updated' },\n\n // Gym structure\n centerAdded: { title: 'Center added' },\n centerUpdated: { title: 'Center updated' },\n membershipPlanCreated: { title: 'Membership plan created' },\n membershipPlanUpdated: { title: 'Membership plan updated' },\n equipmentSaved: { title: 'Equipment updated' },\n\n // Membership (member-side)\n membershipUpdated: { title: 'Membership updated' },\n membershipRenewed: { title: 'Membership renewed' },\n membershipCancelled: { title: 'Membership cancelled' },\n membershipPauseRequested: {\n title: 'Pause request sent',\n description: 'Your gym will confirm the pause.',\n },\n\n // Owner SaaS subscription\n subscriptionUpdated: { title: 'Subscription updated' },\n\n // Training\n workoutPlanCreated: { title: 'Workout plan created' },\n workoutPlanUpdated: { title: 'Workout plan saved' },\n workoutPlanDeleted: {\n title: 'Workout plan deleted',\n description: 'Members on this plan no longer have one.',\n },\n workoutCreated: { title: 'Workout created' },\n workoutUpdated: { title: 'Workout saved' },\n workoutDeleted: { title: 'Workout removed' },\n workoutPlanAssigned: { title: 'Workout plan assigned' },\n\n // Roster (owner console)\n attendanceMarked: { title: 'Attendance saved' },\n memberMarkedInactive: {\n title: 'Member marked inactive',\n description: 'They can no longer check in.',\n },\n memberMarkedActive: { title: 'Member reactivated' },\n memberDeleted: { title: 'Member removed' },\n /**\n * Says what actually happened. Assigning a plan no longer starts it —\n * nothing does until the payment is marked done — so a bare \"Plan\n * updated\" would send the owner away believing the member could train.\n */\n memberPlanAssigned: {\n title: 'Plan assigned',\n description: 'It starts once you mark the payment done.',\n },\n memberMembershipPaused: { title: 'Membership paused' },\n memberMembershipCancelled: { title: 'Membership cancelled' },\n paymentCollected: { title: 'Payment recorded' },\n\n // Sessions\n checkedIn: { title: 'Checked in', description: 'Your session has started.' },\n checkedOut: { title: 'Checked out', description: 'Session saved.' },\n checkInQrRotated: {\n title: 'New QR generated',\n description: 'Print it and replace the old poster.',\n },\n\n // Membership join requests\n membershipRequestSent: {\n title: 'Request sent',\n description: 'The gym will review it shortly.',\n },\n membershipRequestApproved: {\n title: 'Member approved',\n description: 'They can now check in at your gym.',\n },\n membershipRequestRejected: {\n title: 'Request declined',\n description: 'The member has been told.',\n },\n\n // Membership payment confirmation\n membershipRequested: {\n title: 'Plan requested',\n description: 'It starts once your gym confirms the payment.',\n },\n membershipRequestWithdrawn: {\n title: 'Request withdrawn',\n description: 'Pick another plan whenever you are ready.',\n },\n membershipPaymentConfirmed: {\n title: 'Payment confirmed',\n description: 'The plan is now active for this member.',\n },\n membershipPaymentRejected: {\n title: 'Payment turned down',\n description: 'The member has been told and can send it again.',\n },\n\n // Gym registration + platform review\n gymRegistered: {\n title: 'Gym submitted',\n description: 'Our team will review it and let you know.',\n },\n gymApproved: {\n title: 'Gym approved',\n description: 'It is now listed for members to find.',\n },\n gymRejected: {\n title: 'Gym rejected',\n description: 'The owner has been told.',\n },\n\n // Engagement\n feedbackSubmitted: {\n title: 'Feedback sent',\n description: 'Thank you for helping us improve.',\n },\n complaintSubmitted: {\n title: 'Complaint submitted',\n description: 'Your gym will look into it.',\n },\n announcementSent: { title: 'Announcement sent' },\n announcementScheduled: { title: 'Announcement scheduled' },\n announcementDraftSaved: { title: 'Draft saved' },\n announcementDeleted: { title: 'Announcement deleted' },\n announcementCancelled: {\n title: 'Announcement cancelled',\n description: 'It will not be sent.',\n },\n\n // Notification feed\n notificationHidden: { title: 'Notification hidden' },\n notificationRestored: { title: 'Notification restored' },\n allNotificationsRead: { title: 'All caught up' },\n\n // Security (role-switch PIN)\n mpinSet: { title: 'PIN set' },\n mpinReset: { title: 'Your PIN has been reset.' },\n mpinResetLinkSent: {\n title: \"We've emailed you a reset link\",\n description: 'Check your inbox.',\n },\n\n // ── Platform console (superadmin / platform manager) ────────────────────────\n platformRolesUpdated: { title: 'Platform roles updated' },\n platformUserSuspended: {\n title: 'Account suspended',\n description: 'They can no longer sign in.',\n },\n platformUserReinstated: { title: 'Account reinstated' },\n platformUserDeleted: {\n title: 'Account deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformUserRestored: { title: 'Account restored' },\n platformGymDeleted: {\n title: 'Gym deleted',\n description: 'It is hidden everywhere but can still be restored.',\n },\n platformGymRestored: {\n title: 'Gym restored',\n description: 'It is back in the directory and its owner has the console again.',\n },\n} as const satisfies Record<string, ResultMessage>;\n\nexport type SuccessMessageKey = keyof typeof SUCCESS_MESSAGE;\n\n/** Success messages that name the thing they acted on. */\nexport const SUCCESS_MESSAGE_FOR = {\n memberAdded: (name: string): ResultMessage => ({\n title: 'Member added',\n description: `Share the invite with ${name}.`,\n }),\n staffAdded: (name: string): ResultMessage => ({\n title: 'Staff added',\n description: `Share the invite with ${name}.`,\n }),\n inviteClaimed: (gymName: string): ResultMessage => ({\n title: \"You're all set\",\n description: `Your account is linked to ${gymName}.`,\n }),\n roleSwitched: (roleLabel: string): ResultMessage => ({\n title: `Switched to ${roleLabel}`,\n }),\n gymApprovedFor: (gymName: string): ResultMessage => ({\n title: 'Gym approved',\n description: `${gymName} is now live on GymMonk.`,\n }),\n gymRejectedFor: (gymName: string): ResultMessage => ({\n title: 'Gym rejected',\n description: `The owner of ${gymName} has been told why.`,\n }),\n} as const;\n\n// ─── Error messages ──────────────────────────────────────────────────────────\n\n/**\n * Failure copy. Transport entries describe a request that never reached the\n * API; the rest are domain rules the backend enforces and the client can also\n * check up-front — same wording either way.\n */\nexport const ERROR_MESSAGE = {\n // Transport / generic\n generic: 'Something went wrong. Please try again.',\n network: 'Network error. Check your connection and try again.',\n timeout: 'That took too long. Please try again.',\n sessionExpired: 'Your session expired. Please sign in again.',\n\n // Client-side validation\n formInvalid: 'Please check the highlighted fields.',\n\n // Onboarding\n roleRequired: 'Choose a role to continue',\n ownerOnboardingFailed: 'We could not create your gym. Please try again.',\n memberOnboardingFailed: 'We could not save your details. Please try again.',\n\n // ── Gym registration (\"list your gym\") ─────────────────────────────────────\n gymRegistrationFailed: 'We could not register your gym. Please try again.',\n /**\n * The wizard could not read what the owner had already saved.\n *\n * It refuses to open on a blank form rather than risk the next Continue\n * overwriting a real draft, so this is the whole screen, not a toast.\n */\n gymRegistrationDraftLoadFailed:\n 'We could not open your saved gym details. Check your connection and try again.',\n /**\n * The step the wizard tried to save is implausibly large.\n *\n * A guard against a client sending something absurd rather than a limit the\n * real form approaches, but it is raised by the SERVER and shown verbatim in\n * the step-save toast — so the wording lives here with the rest of the copy,\n * not as a hand-written string at the throw site.\n */\n gymRegistrationDraftTooLarge:\n 'Those gym details are too large to save. Please shorten the descriptions and try again.',\n /**\n * \"Near me\" found nothing inside the radius.\n *\n * Deliberately not the same as `gymSearchNoResults`: this member searched\n * nowhere in particular, so telling them to refine their search would be\n * advice they cannot act on. What helps is a different place or a gym id.\n */\n gymSearchNoneNearby:\n 'No gyms within 50 km of you yet. Try a different location, or enter a gym id if you have one.',\n /** One organisation per account — registering a second is not supported. */\n gymAlreadyRegistered: 'You have already registered a gym on this account.',\n /** Registering a gym is something a MEMBER does, so the profile must exist. */\n gymRegistrationNeedsProfile: 'Finish setting up your profile before registering a gym.',\n /** Every owner action other than editing their own details, before approval. */\n gymAwaitingApproval:\n 'Your gym is waiting for approval. You can manage it once our team has reviewed it.',\n /** The reviewer turned the registration down. */\n gymRegistrationRejected: 'Your gym registration was not approved.',\n organisationNotFound: 'That gym could not be found.',\n\n // ── Platform console ────────────────────────────────────────────────────────\n /** The account is gone, or was already erased. */\n platformUserNotFound: 'That account could not be found.',\n /**\n * An admin acting on their OWN row. Suspending or deleting yourself takes away\n * the access needed to undo it, so the console refuses rather than letting one\n * mis-tap lock the platform's last superadmin out of it.\n */\n platformCannotActOnSelf: 'You cannot do that to your own account.',\n /**\n * Removing the last superadmin. `manage GymApproval` is held by nobody else,\n * so the gym review queue would become unreachable with no way back in short\n * of a database edit.\n */\n platformLastSuperadmin: 'The platform must keep at least one superadmin.',\n /** The typed confirmation did not match the record being deleted. */\n platformConfirmationMismatch: 'That did not match. Nothing was changed.',\n /** Two reviewers acting on the same registration; the second one loses. */\n organisationAlreadyDecided: 'Someone has already reviewed this gym.',\n /** A step failed to persist — the user stays put so nothing is lost. */\n onboardingStepSaveFailed: 'We could not save this step. Please try again.',\n\n // Gym structure prerequisites\n centerRequiredForPlan: 'Pick the center this plan belongs to.',\n planVariantRequired: 'Add at least one pricing variant before saving the plan.',\n planNameTaken: 'This center already has a plan with that name. Pick another name.',\n planVariantDurationRequired: 'Enter how long this variant runs for',\n planVariantPriceRequired: 'Enter the price for this variant',\n /** A \"discount\" that raises the price is a typo, every time. */\n planFinalPriceAboveOriginal: 'The final price cannot be more than the original price',\n centerAddressRequired: 'Add the center address before creating it.',\n noCenterForPlans: 'No gym center found. Add a center before creating plans.',\n noCenterForWorkouts: 'No gym center found. Add a center before creating workouts.',\n noCenterForAnnouncements: 'No gym center found. Add a center before sending announcements.',\n announcementAlreadySent: 'That announcement has already gone out, so it cannot be cancelled.',\n\n // Notification feed\n notificationHideFailed: 'Could not hide that notification.',\n notificationRestoreFailed: 'Could not restore that notification.',\n\n // Membership\n planUnavailable: 'That plan is not available right now.',\n membershipPlanRequired: 'Choose a membership plan to continue',\n /** The member was created, but the plan the owner picked did not stick. */\n initialPlanNotAssigned:\n 'Member added, but the plan could not be assigned. Assign it from their profile.',\n noPlansToAssign: 'No membership plans yet. Create one to assign it while adding a member.',\n /**\n * Not an error the member can act on by trying harder — it is the whole\n * point of the flow, so it reads as a status rather than a refusal.\n */\n membershipAwaitingPayment:\n 'Your gym is checking your payment. The plan starts as soon as they confirm it.',\n membershipNoPendingRequest: 'There is no plan waiting for confirmation.',\n /**\n * Pausing extends the term by the paused days, so pausing something that is\n * not running would push out a date nobody is training against — and pausing\n * an already-paused membership would extend it twice for one absence.\n */\n membershipNotPausable:\n 'Only a running membership can be paused. This one has already ended or is paused.',\n /**\n * The joining date records something that has already happened, so a future\n * one is not a preference to honour — it would date the member's whole\n * billing cycle from a day that has not arrived.\n */\n joiningDateInFuture: 'A joining date cannot be in the future.',\n /**\n * The term moved between the screen being drawn and the button being\n * pressed, which in practice means a second manager confirmed one first.\n * Says which month it now covers rather than a bare \"try again\", because the\n * owner's next question is always \"then what am I paying for?\".\n */\n membershipTermMoved:\n 'Someone just confirmed a payment for this member, so the period has moved on. Reopen their profile to see the current one.',\n /**\n * The owner reached for the roster toggle on a plan that has not been paid\n * for. Points at the action that WOULD start it rather than just refusing.\n */\n membershipPendingUseConfirm:\n 'This plan is waiting for payment confirmation. Mark the payment done to start it.',\n /** Two managers opening the same request; the second one loses. */\n membershipAlreadyDecided: 'Someone has already answered this payment.',\n membershipConfirmFailed: 'Could not confirm that payment. Please try again.',\n membershipRejectFailed: 'Could not turn down that payment. Please try again.',\n membershipWithdrawFailed: 'Could not withdraw that request. Please try again.',\n /** The URL did not come from our own storage — a broken or hostile client. */\n paymentProofInvalid: 'That payment screenshot could not be attached. Upload it again.',\n\n // Training\n workoutPlanRequired: 'Choose a plan to assign',\n // Roster (owner console)\n attendanceMarkFailed: 'Could not save that attendance. Please try again.',\n memberStatusFailed: \"Could not change that member's status. Please try again.\",\n memberDeleteFailed: 'Could not remove that member. Please try again.',\n memberPlanAssignFailed: 'Could not update that plan. Please try again.',\n memberPauseFailed: 'Could not pause that membership. Please try again.',\n memberCancelFailed: 'Could not cancel that membership. Please try again.',\n paymentCollectFailed: 'Could not record that payment. Please try again.',\n paymentAmountRequired: 'Enter the amount collected',\n pauseDatesRequired: 'Pick both pause dates',\n pauseDatesOrder: 'The end date has to be after the start date',\n /**\n * Attendance, workout plans and payments are all keyed on the central-auth\n * user, which an invited record does not have until the person claims it.\n */\n memberNotClaimed: 'This member has not claimed their invite yet.',\n\n // Sessions\n checkInFailed: 'Could not check you in. Please try again.',\n checkOutFailed: 'Could not check you out. Please try again.',\n /** The camera decoded a QR, but it is not a gymmonk check-in poster. */\n qrNotRecognised: 'That is not a gym check-in QR. Point at the poster at the gym.',\n /** A valid gymmonk QR, but for a gym the member does not belong to. */\n qrWrongGym: 'That QR belongs to a different gym.',\n /** The scanned code matches no center — a retired or mistyped poster. */\n qrUnknown: 'This QR is no longer valid. Ask the gym for the current poster.',\n /** Checking out with a poster from somewhere other than where you started. */\n qrWrongGymForCheckOut: 'Scan the QR at the gym where you checked in.',\n /** Manual entry only accepts a full, well-formed code. */\n checkInCodeInvalid: 'Enter the 16-character code printed under the QR.',\n checkInQrRotateFailed: 'Could not generate a new QR. Please try again.',\n /**\n * Ticking an exercise off records it against a live session, so there has to\n * be one. Shown when a member opens an exercise outside a workout and taps\n * Mark As Done.\n */\n noSessionToTrack: 'Check in at your gym to track this workout.',\n\n // ── Membership join requests ───────────────────────────────────────────────\n /** Scanned a valid poster, but the gym has not accepted this member yet. */\n membershipNotApproved:\n 'Your gym has not accepted your join request yet. You can check in once they do.',\n /** Accepted, but with no live plan — check-in needs both. */\n membershipNotActive: 'Your membership plan is not active. Renew it to start checking in again.',\n /** A second request while one is already open. */\n membershipRequestPending: 'You already have a request waiting with this gym.',\n /** Requesting a gym they are already an accepted member of. */\n membershipRequestAlreadyMember: 'You are already a member of this gym.',\n /** v1 memberships are single-center — leave the current gym first. */\n membershipRequestAlreadyAtAnotherGym:\n 'You already belong to a gym. Cancel that membership before joining another.',\n membershipRequestNotFound: 'That join request no longer exists.',\n /** Two staff acting on the same request; the second one loses. */\n membershipRequestAlreadyDecided: 'Someone has already answered this request.',\n membershipRequestFailed: 'Could not send your request. Please try again.',\n gymCodeUnknown: 'No gym found with that id. Check it with your gym.',\n /** Actions that only make sense once a gym has taken the member on. */\n noGymJoined: 'Join a gym first. You can find one from your Me tab.',\n /** Searching by name needs somewhere to look — see `gymSearchQuerySchema`. */\n gymSearchStateRequired: 'Pick a state to search gyms by name.',\n gymSearchNoResults: 'No gyms matched. Try a different area, or enter the gym id.',\n\n /**\n * Camera (QR scanning)\n *\n * ── THESE USED TO POINT AT A BOX THAT DOES NOT EXIST ──────────────────────\n * Two of them ended \"Enter the code printed under the QR\", and both halves of\n * that sentence were wrong: the scanner has no field to type a code into, and\n * the poster does not print the check-in code at all — it prints the gym's\n * PUBLIC join code, which cannot check anybody in. So a member whose camera\n * failed was sent looking for a box that is not there, to type a number that\n * is not printed.\n *\n * They now point at the thing that IS there: the check-in button under the\n * viewfinder, which needs no code at all.\n */\n cameraDenied:\n 'Camera access is blocked. Allow it in your browser settings, or check in without scanning.',\n cameraUnavailable: 'No camera found on this device. Check in without scanning instead.',\n cameraInsecure: 'Scanning needs a secure (https) connection.',\n cameraFailed: 'Could not start the camera. Check in without scanning instead.',\n /**\n * The member is not attached to a gym, on the codeless path.\n *\n * Its own sentence because that path reached for `qrUnknown` before it had\n * one, and that string opens \"This QR is no longer valid\" — advice about a\n * poster, given to somebody who never scanned one.\n */\n gymUnavailable: 'You are not checked in to a gym yet. Join one from the Me tab first.',\n\n // Engagement\n feedbackRatingRequired: 'Choose a rating to submit your feedback',\n complaintIssueRequired: 'Choose the issue you are reporting',\n\n // Security (role-switch PIN)\n mpinIncorrect: 'Incorrect PIN.',\n mpinVerifyFailed: \"Couldn't verify. Try again.\",\n mpinSetFailed: \"Couldn't set your PIN. Try again.\",\n mpinMismatch: \"Those didn't match. Try again.\",\n mpinResetLinkFailed: \"Couldn't send the reset link. Try again.\",\n mpinResetFailed: \"Couldn't reset your PIN. Try again.\",\n\n // Uploads\n imageUploadFailed: 'Could not upload that image. Please try again.',\n imageProcessFailed: 'Could not process that image. Please try again.',\n} as const satisfies Record<string, string>;\n\nexport type ErrorMessageKey = keyof typeof ERROR_MESSAGE;\n\n/** Error messages that name a limit or value. */\nexport const ERROR_MESSAGE_FOR = {\n maxPhotos: (max: number): string => `You can add up to ${max} photos`,\n /**\n * Refused at the door because the membership is paused and still inside its\n * window.\n *\n * Its own sentence because \"renew your plan\" is wrong advice here twice\n * over: nothing has lapsed, and paying again would not open the door a day\n * sooner. The resume date is the only thing this member wants to know, and\n * it is the reason this one takes a parameter at all.\n */\n membershipPausedUntil: (resumesOn: string): string =>\n `Your membership is paused until ${resumesOn}. You can check in again from that day.`,\n /** Role-switch PIN lockout, in whole minutes remaining. */\n mpinLockedOut: (minutes: number): string =>\n `Too many attempts. Try again in ${minutes} minute${minutes > 1 ? 's' : ''}.`,\n} as const;\n"]}
package/dist/session.d.ts CHANGED
@@ -103,6 +103,44 @@ export declare const checkOutBodySchema: z.ZodObject<{
103
103
  code: z.ZodString;
104
104
  }, z.core.$strip>;
105
105
  export type CheckOutBody = z.infer<typeof checkOutBodySchema>;
106
+ /**
107
+ * Checking in WITHOUT the poster — the button under the viewfinder.
108
+ *
109
+ * ── Why this exists ─────────────────────────────────────────────────────────
110
+ * A camera that will not open is not rare: permission blocked months ago on a
111
+ * different screen, a browser without `getUserMedia`, a cracked lens, a poster
112
+ * gone matte under gym lighting. Every one of those left a paying member
113
+ * standing at the desk with no way to record that they came. The scanner's own
114
+ * error copy used to send them looking for "the code printed under the QR" —
115
+ * a box the scanner does not have, for a number the poster does not print.
116
+ *
117
+ * ── Why the position is OPTIONAL ────────────────────────────────────────────
118
+ * It reads as a contradiction — the whole point of this path is that there is
119
+ * no poster to prove anything, so surely the position must — but requiring it
120
+ * would rebuild the lockout one door along. The member most likely to need
121
+ * this button is the one whose browser is already refusing hardware, and
122
+ * "location" is the same permission stack as "camera". Demanding a fix here
123
+ * turns the fallback into a second thing that can refuse them.
124
+ *
125
+ * So the position is taken when it is there and recorded either way. Where the
126
+ * member stood lands on the attendance row as `atGym` — true, false, or "we
127
+ * could not tell" — and the row is stamped `self`, so an owner reading the
128
+ * sheet sees an arrival nobody witnessed and can act on it. Evidence, and a
129
+ * person deciding. Never a gate.
130
+ */
131
+ export declare const manualCheckInBodySchema: z.ZodObject<{
132
+ lat: z.ZodOptional<z.ZodNumber>;
133
+ lng: z.ZodOptional<z.ZodNumber>;
134
+ accuracyM: z.ZodOptional<z.ZodNumber>;
135
+ }, z.core.$strip>;
136
+ export type ManualCheckInBody = z.infer<typeof manualCheckInBodySchema>;
137
+ /** Leaving, the same way. Same shape, and the same reasons. */
138
+ export declare const manualCheckOutBodySchema: z.ZodObject<{
139
+ lat: z.ZodOptional<z.ZodNumber>;
140
+ lng: z.ZodOptional<z.ZodNumber>;
141
+ accuracyM: z.ZodOptional<z.ZodNumber>;
142
+ }, z.core.$strip>;
143
+ export type ManualCheckOutBody = z.infer<typeof manualCheckOutBodySchema>;
106
144
  export declare const markExerciseDoneBodySchema: z.ZodObject<{
107
145
  exerciseId: z.ZodString;
108
146
  done: z.ZodBoolean;
@@ -134,10 +172,21 @@ export type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;
134
172
  * How an attendance mark came to be recorded.
135
173
  *
136
174
  * `scan` — the member scanned the gym's QR poster themselves.
175
+ * `self` — the member marked themselves present WITHOUT scanning, from the
176
+ * button under the viewfinder, because the camera would not open or
177
+ * the poster would not read.
137
178
  * `manual` — a person put it there: an owner, manager or front desk.
138
179
  * `auto` — nobody did. The member never scanned out and the session hit its
139
180
  * cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).
140
181
  *
182
+ * `self` is deliberately its OWN value rather than folded into either
183
+ * neighbour. It is not `scan`: no poster was read, so the strongest evidence
184
+ * this system has is absent. It is not `manual` either: `manual` means a member
185
+ * of staff vouched for the member and carries their NAME, and quietly borrowing
186
+ * that would attribute an unwitnessed arrival to a person who never saw it.
187
+ * Naming it is what lets an owner scan a sheet and see which arrivals were
188
+ * proved by a poster and which were asserted.
189
+ *
141
190
  * Recorded per SIDE, because the two halves of a visit routinely differ: the
142
191
  * common case is a member who scans in and is closed out automatically. An
143
192
  * attendance report that cannot tell a scanned departure from an assumed one is
@@ -145,6 +194,7 @@ export type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;
145
194
  */
146
195
  export declare const attendanceSourceSchema: z.ZodEnum<{
147
196
  scan: "scan";
197
+ self: "self";
148
198
  manual: "manual";
149
199
  auto: "auto";
150
200
  }>;
@@ -167,11 +217,13 @@ export declare const attendanceEntrySchema: z.ZodObject<{
167
217
  }>;
168
218
  checkInSource: z.ZodNullable<z.ZodEnum<{
169
219
  scan: "scan";
220
+ self: "self";
170
221
  manual: "manual";
171
222
  auto: "auto";
172
223
  }>>;
173
224
  checkOutSource: z.ZodNullable<z.ZodEnum<{
174
225
  scan: "scan";
226
+ self: "self";
175
227
  manual: "manual";
176
228
  auto: "auto";
177
229
  }>>;
@@ -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;;;;;;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;iBA0ChC,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"}
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;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,uBAAuB;;;;iBAIlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,+DAA+D;AAC/D,eAAO,MAAM,wBAAwB;;;;iBAA0B,CAAC;AAChE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,sBAAsB;;;;;EAA6C,CAAC;AACjF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;iBA0ChC,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
@@ -104,6 +104,38 @@ export const checkInBodySchema = z
104
104
  export const checkOutBodySchema = z.object({
105
105
  code: checkInCodeSchema,
106
106
  });
107
+ /**
108
+ * Checking in WITHOUT the poster — the button under the viewfinder.
109
+ *
110
+ * ── Why this exists ─────────────────────────────────────────────────────────
111
+ * A camera that will not open is not rare: permission blocked months ago on a
112
+ * different screen, a browser without `getUserMedia`, a cracked lens, a poster
113
+ * gone matte under gym lighting. Every one of those left a paying member
114
+ * standing at the desk with no way to record that they came. The scanner's own
115
+ * error copy used to send them looking for "the code printed under the QR" —
116
+ * a box the scanner does not have, for a number the poster does not print.
117
+ *
118
+ * ── Why the position is OPTIONAL ────────────────────────────────────────────
119
+ * It reads as a contradiction — the whole point of this path is that there is
120
+ * no poster to prove anything, so surely the position must — but requiring it
121
+ * would rebuild the lockout one door along. The member most likely to need
122
+ * this button is the one whose browser is already refusing hardware, and
123
+ * "location" is the same permission stack as "camera". Demanding a fix here
124
+ * turns the fallback into a second thing that can refuse them.
125
+ *
126
+ * So the position is taken when it is there and recorded either way. Where the
127
+ * member stood lands on the attendance row as `atGym` — true, false, or "we
128
+ * could not tell" — and the row is stamped `self`, so an owner reading the
129
+ * sheet sees an arrival nobody witnessed and can act on it. Evidence, and a
130
+ * person deciding. Never a gate.
131
+ */
132
+ export const manualCheckInBodySchema = z.object({
133
+ lat: z.number().min(-90).max(90).optional(),
134
+ lng: z.number().min(-180).max(180).optional(),
135
+ accuracyM: z.number().min(0).max(100_000).optional(),
136
+ });
137
+ /** Leaving, the same way. Same shape, and the same reasons. */
138
+ export const manualCheckOutBodySchema = manualCheckInBodySchema;
107
139
  export const markExerciseDoneBodySchema = z.object({
108
140
  exerciseId: objectIdSchema,
109
141
  done: z.boolean(),
@@ -125,16 +157,27 @@ export const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);
125
157
  * How an attendance mark came to be recorded.
126
158
  *
127
159
  * `scan` — the member scanned the gym's QR poster themselves.
160
+ * `self` — the member marked themselves present WITHOUT scanning, from the
161
+ * button under the viewfinder, because the camera would not open or
162
+ * the poster would not read.
128
163
  * `manual` — a person put it there: an owner, manager or front desk.
129
164
  * `auto` — nobody did. The member never scanned out and the session hit its
130
165
  * cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).
131
166
  *
167
+ * `self` is deliberately its OWN value rather than folded into either
168
+ * neighbour. It is not `scan`: no poster was read, so the strongest evidence
169
+ * this system has is absent. It is not `manual` either: `manual` means a member
170
+ * of staff vouched for the member and carries their NAME, and quietly borrowing
171
+ * that would attribute an unwitnessed arrival to a person who never saw it.
172
+ * Naming it is what lets an owner scan a sheet and see which arrivals were
173
+ * proved by a poster and which were asserted.
174
+ *
132
175
  * Recorded per SIDE, because the two halves of a visit routinely differ: the
133
176
  * common case is a member who scans in and is closed out automatically. An
134
177
  * attendance report that cannot tell a scanned departure from an assumed one is
135
178
  * reporting a guess as a measurement.
136
179
  */
137
- export const attendanceSourceSchema = z.enum(['scan', 'manual', 'auto']);
180
+ export const attendanceSourceSchema = z.enum(['scan', 'self', 'manual', 'auto']);
138
181
  /**
139
182
  * How long a session may stay open before it is closed for the member.
140
183
  *
@@ -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\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\n /**\n * Was the member at the gym when they scanned? THREE answers, never two.\n *\n * `true` we measured, and they were within the gym's radius.\n * `false` we measured, and they were not.\n * `null` we could not measure — nobody pinned the gym, or the phone would\n * not give a fix.\n *\n * Nullable is the whole point. A gym that has never been pinned and a member\n * standing at the front desk must not read the same, and collapsing\n * \"unknown\" into either answer invents a fact: into `true` it launders a\n * guess into a verification, into `false` it accuses somebody over a\n * measurement nobody took.\n *\n * ── This never refuses a check-in ───────────────────────────────────────────\n * It is a REPORT, not a gate, and the ONLY thing location does in this\n * product. Scanning always succeeds. A phone indoors, behind a concrete wall\n * and a floor of equipment, routinely reports a fix 100-150 m out — so\n * refusing on this locked members out of the gym they pay for while catching\n * nobody, since spoofing a coordinate is cheaper than the walk it was meant\n * to enforce. The owner sees the flag and decides.\n */\n atGym: z.boolean().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"]}
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\n/**\n * Checking in WITHOUT the poster — the button under the viewfinder.\n *\n * ── Why this exists ─────────────────────────────────────────────────────────\n * A camera that will not open is not rare: permission blocked months ago on a\n * different screen, a browser without `getUserMedia`, a cracked lens, a poster\n * gone matte under gym lighting. Every one of those left a paying member\n * standing at the desk with no way to record that they came. The scanner's own\n * error copy used to send them looking for \"the code printed under the QR\" —\n * a box the scanner does not have, for a number the poster does not print.\n *\n * ── Why the position is OPTIONAL ────────────────────────────────────────────\n * It reads as a contradiction — the whole point of this path is that there is\n * no poster to prove anything, so surely the position must — but requiring it\n * would rebuild the lockout one door along. The member most likely to need\n * this button is the one whose browser is already refusing hardware, and\n * \"location\" is the same permission stack as \"camera\". Demanding a fix here\n * turns the fallback into a second thing that can refuse them.\n *\n * So the position is taken when it is there and recorded either way. Where the\n * member stood lands on the attendance row as `atGym` — true, false, or \"we\n * could not tell\" — and the row is stamped `self`, so an owner reading the\n * sheet sees an arrival nobody witnessed and can act on it. Evidence, and a\n * person deciding. Never a gate.\n */\nexport const manualCheckInBodySchema = z.object({\n lat: z.number().min(-90).max(90).optional(),\n lng: z.number().min(-180).max(180).optional(),\n accuracyM: z.number().min(0).max(100_000).optional(),\n});\nexport type ManualCheckInBody = z.infer<typeof manualCheckInBodySchema>;\n\n/** Leaving, the same way. Same shape, and the same reasons. */\nexport const manualCheckOutBodySchema = manualCheckInBodySchema;\nexport type ManualCheckOutBody = z.infer<typeof manualCheckOutBodySchema>;\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 * `self` — the member marked themselves present WITHOUT scanning, from the\n * button under the viewfinder, because the camera would not open or\n * the poster would not read.\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 * `self` is deliberately its OWN value rather than folded into either\n * neighbour. It is not `scan`: no poster was read, so the strongest evidence\n * this system has is absent. It is not `manual` either: `manual` means a member\n * of staff vouched for the member and carries their NAME, and quietly borrowing\n * that would attribute an unwitnessed arrival to a person who never saw it.\n * Naming it is what lets an owner scan a sheet and see which arrivals were\n * proved by a poster and which were asserted.\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', 'self', '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\n /**\n * Was the member at the gym when they scanned? THREE answers, never two.\n *\n * `true` we measured, and they were within the gym's radius.\n * `false` we measured, and they were not.\n * `null` we could not measure — nobody pinned the gym, or the phone would\n * not give a fix.\n *\n * Nullable is the whole point. A gym that has never been pinned and a member\n * standing at the front desk must not read the same, and collapsing\n * \"unknown\" into either answer invents a fact: into `true` it launders a\n * guess into a verification, into `false` it accuses somebody over a\n * measurement nobody took.\n *\n * ── This never refuses a check-in ───────────────────────────────────────────\n * It is a REPORT, not a gate, and the ONLY thing location does in this\n * product. Scanning always succeeds. A phone indoors, behind a concrete wall\n * and a floor of equipment, routinely reports a fix 100-150 m out — so\n * refusing on this locked members out of the gym they pay for while catching\n * nobody, since spoofing a coordinate is cheaper than the walk it was meant\n * to enforce. The owner sees the flag and decides.\n */\n atGym: z.boolean().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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gymmonk-schema",
3
- "version": "0.49.0",
3
+ "version": "0.51.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",