gymmonk-schema 0.25.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/announcement.d.ts +29 -41
- package/dist/announcement.d.ts.map +1 -1
- package/dist/announcement.js +34 -64
- package/dist/announcement.js.map +1 -1
- package/dist/area.d.ts.map +1 -1
- package/dist/area.js +7 -1
- package/dist/area.js.map +1 -1
- package/dist/session.d.ts +38 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +34 -0
- package/dist/session.js.map +1 -1
- package/package.json +1 -1
package/dist/announcement.d.ts
CHANGED
|
@@ -18,9 +18,23 @@ export declare const announcementIconSchema: z.ZodEnum<{
|
|
|
18
18
|
warning: "warning";
|
|
19
19
|
}>;
|
|
20
20
|
export type AnnouncementIcon = z.infer<typeof announcementIconSchema>;
|
|
21
|
+
/**
|
|
22
|
+
* An announcement is a PRESENT EVENT.
|
|
23
|
+
*
|
|
24
|
+
* `scheduled` is gone. A broadcast now goes out the moment it is created, which
|
|
25
|
+
* removes the whole apparatus that existed to make "later" work: the due sweep,
|
|
26
|
+
* the claim protocol, the past-date guard, and the Cloud Scheduler job that had
|
|
27
|
+
* to wake a scale-to-zero service every minute for it to mean anything. That
|
|
28
|
+
* job never existed in production, so every scheduled announcement was in fact
|
|
29
|
+
* delivered "the next time somebody opened the app" — the feature promised a
|
|
30
|
+
* time and could not keep it.
|
|
31
|
+
*
|
|
32
|
+
* `draft` stays, and is not a contradiction: a draft is not a queued
|
|
33
|
+
* announcement, it is an unsent note. Sending it is still a present event, it
|
|
34
|
+
* just happens when the owner presses the button rather than when they save.
|
|
35
|
+
*/
|
|
21
36
|
export declare const announcementStatusSchema: z.ZodEnum<{
|
|
22
37
|
draft: "draft";
|
|
23
|
-
scheduled: "scheduled";
|
|
24
38
|
sent: "sent";
|
|
25
39
|
}>;
|
|
26
40
|
export type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;
|
|
@@ -35,30 +49,21 @@ export declare const announcementSchema: z.ZodObject<{
|
|
|
35
49
|
body: z.ZodString;
|
|
36
50
|
status: z.ZodEnum<{
|
|
37
51
|
draft: "draft";
|
|
38
|
-
scheduled: "scheduled";
|
|
39
52
|
sent: "sent";
|
|
40
53
|
}>;
|
|
41
|
-
scheduledAt: z.ZodNullable<z.ZodISODateTime>;
|
|
42
54
|
sentAt: z.ZodNullable<z.ZodISODateTime>;
|
|
43
55
|
deliveredCount: z.ZodNullable<z.ZodNumber>;
|
|
44
|
-
deliveryFailedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
45
56
|
createdAt: z.ZodISODateTime;
|
|
46
57
|
updatedAt: z.ZodISODateTime;
|
|
47
58
|
}, z.core.$strip>;
|
|
48
59
|
export type Announcement = z.infer<typeof announcementSchema>;
|
|
49
60
|
/**
|
|
50
|
-
*
|
|
61
|
+
* Write one and it goes out, unless it is explicitly saved as a draft.
|
|
51
62
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Refusing that would be a validation error for doing exactly the right thing.
|
|
56
|
-
* Anything older is a genuine mistake: it would be delivered by the very next
|
|
57
|
-
* sweep while the screen called it Scheduled.
|
|
63
|
+
* No `scheduledAt`, and no refinements: with delivery immediate there is
|
|
64
|
+
* nothing left to cross-validate. The two that used to be here both existed to
|
|
65
|
+
* police a future time that no longer exists.
|
|
58
66
|
*/
|
|
59
|
-
export declare const SCHEDULE_GRACE_MS: number;
|
|
60
|
-
export declare const PAST_SCHEDULE_MESSAGE = "Pick a time in the future";
|
|
61
|
-
export declare function isScheduleInPast(iso: string, now?: Date): boolean;
|
|
62
67
|
export declare const createAnnouncementBodySchema: z.ZodObject<{
|
|
63
68
|
icon: z.ZodDefault<z.ZodEnum<{
|
|
64
69
|
megaphone: "megaphone";
|
|
@@ -66,43 +71,34 @@ export declare const createAnnouncementBodySchema: z.ZodObject<{
|
|
|
66
71
|
}>>;
|
|
67
72
|
title: z.ZodString;
|
|
68
73
|
body: z.ZodString;
|
|
69
|
-
scheduledAt: z.ZodOptional<z.ZodISODateTime>;
|
|
70
74
|
status: z.ZodDefault<z.ZodEnum<{
|
|
71
75
|
draft: "draft";
|
|
72
|
-
scheduled: "scheduled";
|
|
73
76
|
sent: "sent";
|
|
74
77
|
}>>;
|
|
75
78
|
}, z.core.$strip>;
|
|
76
79
|
export type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;
|
|
77
80
|
/** The create body plus the center it belongs to, as the route receives it. */
|
|
78
|
-
export declare const createAnnouncementWithCenterSchema: z.
|
|
81
|
+
export declare const createAnnouncementWithCenterSchema: z.ZodObject<{
|
|
79
82
|
icon: z.ZodDefault<z.ZodEnum<{
|
|
80
83
|
megaphone: "megaphone";
|
|
81
84
|
warning: "warning";
|
|
82
85
|
}>>;
|
|
83
86
|
title: z.ZodString;
|
|
84
87
|
body: z.ZodString;
|
|
85
|
-
scheduledAt: z.ZodOptional<z.ZodISODateTime>;
|
|
86
88
|
status: z.ZodDefault<z.ZodEnum<{
|
|
87
89
|
draft: "draft";
|
|
88
|
-
scheduled: "scheduled";
|
|
89
90
|
sent: "sent";
|
|
90
91
|
}>>;
|
|
91
|
-
}, z.core.$strip>, z.ZodObject<{
|
|
92
92
|
centerId: z.ZodString;
|
|
93
|
-
}, z.core.$strip
|
|
93
|
+
}, z.core.$strip>;
|
|
94
94
|
export type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;
|
|
95
95
|
/**
|
|
96
|
-
* Edit a draft,
|
|
96
|
+
* Edit a draft, and send it.
|
|
97
97
|
*
|
|
98
98
|
* Every field is optional because this is a PATCH: an owner fixing a typo sends
|
|
99
|
-
* the title alone. `status` is how a draft finally goes out, which is
|
|
100
|
-
* reason this exists — before it there was no update endpoint at all,
|
|
101
|
-
* as Draft" wrote a row that could only ever be deleted.
|
|
102
|
-
*
|
|
103
|
-
* `scheduledAt` accepts null explicitly, meaning "drop the schedule", which is
|
|
104
|
-
* different from omitting it ("leave the schedule alone"). Conflating the two
|
|
105
|
-
* would make it impossible to turn a scheduled announcement back into a draft.
|
|
99
|
+
* the title alone. `status: 'sent'` is how a draft finally goes out, which is
|
|
100
|
+
* the whole reason this exists — before it there was no update endpoint at all,
|
|
101
|
+
* so "Save as Draft" wrote a row that could only ever be deleted.
|
|
106
102
|
*/
|
|
107
103
|
export declare const updateAnnouncementBodySchema: z.ZodObject<{
|
|
108
104
|
icon: z.ZodOptional<z.ZodEnum<{
|
|
@@ -113,31 +109,25 @@ export declare const updateAnnouncementBodySchema: z.ZodObject<{
|
|
|
113
109
|
body: z.ZodOptional<z.ZodString>;
|
|
114
110
|
status: z.ZodOptional<z.ZodEnum<{
|
|
115
111
|
draft: "draft";
|
|
116
|
-
scheduled: "scheduled";
|
|
117
112
|
sent: "sent";
|
|
118
113
|
}>>;
|
|
119
|
-
scheduledAt: z.ZodOptional<z.ZodNullable<z.ZodISODateTime>>;
|
|
120
114
|
}, z.core.$strip>;
|
|
121
115
|
export type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;
|
|
122
116
|
/**
|
|
123
|
-
* How the list is ordered.
|
|
117
|
+
* How the list is ordered. `newest` by default, because the screen's job is
|
|
118
|
+
* "what did we just send".
|
|
124
119
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* that HAVE one: in a mixed list every draft and every already-sent row sorts as
|
|
128
|
-
* null and groups at the front, so the UI offers it under the Scheduled filter
|
|
129
|
-
* alone.
|
|
120
|
+
* There was a third, `upcoming`, ordering by the scheduled time. It went with
|
|
121
|
+
* scheduling.
|
|
130
122
|
*/
|
|
131
123
|
export declare const announcementSortSchema: z.ZodEnum<{
|
|
132
124
|
newest: "newest";
|
|
133
125
|
oldest: "oldest";
|
|
134
|
-
upcoming: "upcoming";
|
|
135
126
|
}>;
|
|
136
127
|
export type AnnouncementSort = z.infer<typeof announcementSortSchema>;
|
|
137
128
|
/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */
|
|
138
129
|
export declare const announcementStatusFilterSchema: z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodEnum<{
|
|
139
130
|
draft: "draft";
|
|
140
|
-
scheduled: "scheduled";
|
|
141
131
|
sent: "sent";
|
|
142
132
|
}>]>;
|
|
143
133
|
export type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;
|
|
@@ -155,13 +145,11 @@ export declare const listAnnouncementsQuerySchema: z.ZodObject<{
|
|
|
155
145
|
q: z.ZodOptional<z.ZodString>;
|
|
156
146
|
status: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"all">, z.ZodEnum<{
|
|
157
147
|
draft: "draft";
|
|
158
|
-
scheduled: "scheduled";
|
|
159
148
|
sent: "sent";
|
|
160
149
|
}>]>>;
|
|
161
150
|
sort: z.ZodDefault<z.ZodEnum<{
|
|
162
151
|
newest: "newest";
|
|
163
152
|
oldest: "oldest";
|
|
164
|
-
upcoming: "upcoming";
|
|
165
153
|
}>>;
|
|
166
154
|
}, z.core.$strip>;
|
|
167
155
|
export type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,wBAAwB
|
|
1
|
+
{"version":3,"file":"announcement.d.ts","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,sBAAsB;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,wBAAwB;;;EAA4B,CAAC;AAClE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAI1E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;iBAmB7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAI9D;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,+EAA+E;AAC/E,eAAO,MAAM,kCAAkC;;;;;;;;;;;;iBAE7C,CAAC;AACH,MAAM,MAAM,gCAAgC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kCAAkC,CAAC,CAAC;AAIlG;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;iBAKvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAIlF;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB;;;EAA+B,CAAC;AACnE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,iFAAiF;AACjF,eAAO,MAAM,8BAA8B;;;IAAwD,CAAC;AACpG,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,8BAA8B,CAAC,CAAC;AAEtF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;iBAMvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AAElF,kFAAkF;AAClF,eAAO,MAAM,sBAAsB;;iBAAyC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
package/dist/announcement.js
CHANGED
|
@@ -15,7 +15,22 @@
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';
|
|
17
17
|
export const announcementIconSchema = z.enum(['megaphone', 'warning']);
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* An announcement is a PRESENT EVENT.
|
|
20
|
+
*
|
|
21
|
+
* `scheduled` is gone. A broadcast now goes out the moment it is created, which
|
|
22
|
+
* removes the whole apparatus that existed to make "later" work: the due sweep,
|
|
23
|
+
* the claim protocol, the past-date guard, and the Cloud Scheduler job that had
|
|
24
|
+
* to wake a scale-to-zero service every minute for it to mean anything. That
|
|
25
|
+
* job never existed in production, so every scheduled announcement was in fact
|
|
26
|
+
* delivered "the next time somebody opened the app" — the feature promised a
|
|
27
|
+
* time and could not keep it.
|
|
28
|
+
*
|
|
29
|
+
* `draft` stays, and is not a contradiction: a draft is not a queued
|
|
30
|
+
* announcement, it is an unsent note. Sending it is still a present event, it
|
|
31
|
+
* just happens when the owner presses the button rather than when they save.
|
|
32
|
+
*/
|
|
33
|
+
export const announcementStatusSchema = z.enum(['draft', 'sent']);
|
|
19
34
|
// ─── Entity ──────────────────────────────────────────────────────────────────
|
|
20
35
|
export const announcementSchema = z.object({
|
|
21
36
|
id: objectIdSchema,
|
|
@@ -24,7 +39,6 @@ export const announcementSchema = z.object({
|
|
|
24
39
|
title: z.string(),
|
|
25
40
|
body: z.string(),
|
|
26
41
|
status: announcementStatusSchema,
|
|
27
|
-
scheduledAt: isoDateTimeSchema.nullable(),
|
|
28
42
|
sentAt: isoDateTimeSchema.nullable(),
|
|
29
43
|
/**
|
|
30
44
|
* How many people it actually reached, once delivery has been attempted.
|
|
@@ -35,95 +49,51 @@ export const announcementSchema = z.object({
|
|
|
35
49
|
* both to zero would leave the screen unable to say which.
|
|
36
50
|
*/
|
|
37
51
|
deliveredCount: z.number().int().nullable(),
|
|
38
|
-
/**
|
|
39
|
-
* Set when fan-out threw, cleared when a later attempt succeeds.
|
|
40
|
-
*
|
|
41
|
-
* A row is marked `sent` BEFORE fan-out (it prevents a duplicate broadcast to
|
|
42
|
-
* an entire gym), so without this a failure left no trace but a log line while
|
|
43
|
-
* the owner's screen said Sent.
|
|
44
|
-
*/
|
|
45
|
-
deliveryFailedAt: isoDateTimeSchema.nullable(),
|
|
46
52
|
createdAt: isoDateTimeSchema,
|
|
47
53
|
updatedAt: isoDateTimeSchema,
|
|
48
54
|
});
|
|
49
|
-
// ───
|
|
55
|
+
// ─── Create body ─────────────────────────────────────────────────────────────
|
|
50
56
|
/**
|
|
51
|
-
*
|
|
57
|
+
* Write one and it goes out, unless it is explicitly saved as a draft.
|
|
52
58
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Refusing that would be a validation error for doing exactly the right thing.
|
|
57
|
-
* Anything older is a genuine mistake: it would be delivered by the very next
|
|
58
|
-
* sweep while the screen called it Scheduled.
|
|
59
|
+
* No `scheduledAt`, and no refinements: with delivery immediate there is
|
|
60
|
+
* nothing left to cross-validate. The two that used to be here both existed to
|
|
61
|
+
* police a future time that no longer exists.
|
|
59
62
|
*/
|
|
60
|
-
export const
|
|
61
|
-
export const PAST_SCHEDULE_MESSAGE = 'Pick a time in the future';
|
|
62
|
-
export function isScheduleInPast(iso, now = new Date()) {
|
|
63
|
-
const at = Date.parse(iso);
|
|
64
|
-
if (Number.isNaN(at))
|
|
65
|
-
return false; // shape is the format check's problem
|
|
66
|
-
return at < now.getTime() - SCHEDULE_GRACE_MS;
|
|
67
|
-
}
|
|
68
|
-
// ─── Create body ─────────────────────────────────────────────────────────────
|
|
69
|
-
export const createAnnouncementBodySchema = z
|
|
70
|
-
.object({
|
|
63
|
+
export const createAnnouncementBodySchema = z.object({
|
|
71
64
|
icon: announcementIconSchema.default('megaphone'),
|
|
72
65
|
title: z.string().trim().min(1, 'Title is required').max(120),
|
|
73
66
|
body: z.string().trim().min(1, 'Message is required').max(250),
|
|
74
|
-
/** Required when `status` is `scheduled`; otherwise sent immediately. */
|
|
75
|
-
scheduledAt: isoDateTimeSchema.optional(),
|
|
76
67
|
status: announcementStatusSchema.default('sent'),
|
|
77
|
-
})
|
|
78
|
-
.refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {
|
|
79
|
-
message: 'Pick a schedule time',
|
|
80
|
-
path: ['scheduledAt'],
|
|
81
|
-
})
|
|
82
|
-
/**
|
|
83
|
-
* A scheduled time must be in the FUTURE. Without this, yesterday 9am
|
|
84
|
-
* validated cleanly, was stored as `scheduled`, and went out on the next sweep
|
|
85
|
-
* seconds later: the owner saw "Scheduled" and the members got it at once.
|
|
86
|
-
*
|
|
87
|
-
* Drafts are exempt on purpose. Nothing is going out, so a stale time on one
|
|
88
|
-
* is not yet a mistake.
|
|
89
|
-
*/
|
|
90
|
-
.refine((v) => v.status !== 'scheduled' || !v.scheduledAt || !isScheduleInPast(v.scheduledAt), {
|
|
91
|
-
message: PAST_SCHEDULE_MESSAGE,
|
|
92
|
-
path: ['scheduledAt'],
|
|
93
68
|
});
|
|
94
69
|
/** The create body plus the center it belongs to, as the route receives it. */
|
|
95
|
-
export const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.
|
|
70
|
+
export const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.extend({
|
|
71
|
+
centerId: objectIdSchema,
|
|
72
|
+
});
|
|
96
73
|
// ─── Update body ─────────────────────────────────────────────────────────────
|
|
97
74
|
/**
|
|
98
|
-
* Edit a draft,
|
|
75
|
+
* Edit a draft, and send it.
|
|
99
76
|
*
|
|
100
77
|
* Every field is optional because this is a PATCH: an owner fixing a typo sends
|
|
101
|
-
* the title alone. `status` is how a draft finally goes out, which is
|
|
102
|
-
* reason this exists — before it there was no update endpoint at all,
|
|
103
|
-
* as Draft" wrote a row that could only ever be deleted.
|
|
104
|
-
*
|
|
105
|
-
* `scheduledAt` accepts null explicitly, meaning "drop the schedule", which is
|
|
106
|
-
* different from omitting it ("leave the schedule alone"). Conflating the two
|
|
107
|
-
* would make it impossible to turn a scheduled announcement back into a draft.
|
|
78
|
+
* the title alone. `status: 'sent'` is how a draft finally goes out, which is
|
|
79
|
+
* the whole reason this exists — before it there was no update endpoint at all,
|
|
80
|
+
* so "Save as Draft" wrote a row that could only ever be deleted.
|
|
108
81
|
*/
|
|
109
82
|
export const updateAnnouncementBodySchema = z.object({
|
|
110
83
|
icon: announcementIconSchema.optional(),
|
|
111
84
|
title: z.string().trim().min(1, 'Title is required').max(120).optional(),
|
|
112
85
|
body: z.string().trim().min(1, 'Message is required').max(250).optional(),
|
|
113
86
|
status: announcementStatusSchema.optional(),
|
|
114
|
-
scheduledAt: isoDateTimeSchema.nullable().optional(),
|
|
115
87
|
});
|
|
116
88
|
// ─── Listing query ───────────────────────────────────────────────────────────
|
|
117
89
|
/**
|
|
118
|
-
* How the list is ordered.
|
|
90
|
+
* How the list is ordered. `newest` by default, because the screen's job is
|
|
91
|
+
* "what did we just send".
|
|
119
92
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
* that HAVE one: in a mixed list every draft and every already-sent row sorts as
|
|
123
|
-
* null and groups at the front, so the UI offers it under the Scheduled filter
|
|
124
|
-
* alone.
|
|
93
|
+
* There was a third, `upcoming`, ordering by the scheduled time. It went with
|
|
94
|
+
* scheduling.
|
|
125
95
|
*/
|
|
126
|
-
export const announcementSortSchema = z.enum(['newest', 'oldest'
|
|
96
|
+
export const announcementSortSchema = z.enum(['newest', 'oldest']);
|
|
127
97
|
/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */
|
|
128
98
|
export const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);
|
|
129
99
|
/**
|
package/dist/announcement.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"announcement.js","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Owner announcements\n * ====================================\n * Broadcasts an owner sends to a center's members and staff (now or scheduled).\n * Delivered as notifications + push.\n *\n * SINGLE SOURCE OF TRUTH. The listing query, the update body and the\n * past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two\n * hand-kept mirrors before this. They are here so a field can only be added\n * once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,\n * it silently drops the field somewhere between the browser and the database.\n *\n * @module gymmonk-schema/announcement\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';\n\nexport const announcementIconSchema = z.enum(['megaphone', 'warning']);\nexport type AnnouncementIcon = z.infer<typeof announcementIconSchema>;\n\nexport const announcementStatusSchema = z.enum(['draft', 'scheduled', 'sent']);\nexport type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const announcementSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n icon: announcementIconSchema,\n title: z.string(),\n body: z.string(),\n status: announcementStatusSchema,\n scheduledAt: isoDateTimeSchema.nullable(),\n sentAt: isoDateTimeSchema.nullable(),\n /**\n * How many people it actually reached, once delivery has been attempted.\n *\n * `null` means \"not attempted yet\", which is NOT the same as `0` (\"attempted,\n * nobody to tell\"). An owner broadcasting to an empty roster and an owner\n * whose broadcast has not run yet are different situations, and collapsing\n * both to zero would leave the screen unable to say which.\n */\n deliveredCount: z.number().int().nullable(),\n /**\n * Set when fan-out threw, cleared when a later attempt succeeds.\n *\n * A row is marked `sent` BEFORE fan-out (it prevents a duplicate broadcast to\n * an entire gym), so without this a failure left no trace but a log line while\n * the owner's screen said Sent.\n */\n deliveryFailedAt: isoDateTimeSchema.nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Announcement = z.infer<typeof announcementSchema>;\n\n// ─── Scheduling guard ────────────────────────────────────────────────────────\n\n/**\n * How far in the past a `scheduledAt` may sit and still be treated as \"now\".\n *\n * Not zero. The browser builds the instant from two wall-clock inputs and the\n * request then crosses a network, so an owner who picks the next minute and\n * taps promptly can arrive a few seconds \"late\" through no fault of their own.\n * Refusing that would be a validation error for doing exactly the right thing.\n * Anything older is a genuine mistake: it would be delivered by the very next\n * sweep while the screen called it Scheduled.\n */\nexport const SCHEDULE_GRACE_MS = 2 * 60 * 1000;\n\nexport const PAST_SCHEDULE_MESSAGE = 'Pick a time in the future';\n\nexport function isScheduleInPast(iso: string, now: Date = new Date()): boolean {\n const at = Date.parse(iso);\n if (Number.isNaN(at)) return false; // shape is the format check's problem\n return at < now.getTime() - SCHEDULE_GRACE_MS;\n}\n\n// ─── Create body ─────────────────────────────────────────────────────────────\n\nexport const createAnnouncementBodySchema = z\n .object({\n icon: announcementIconSchema.default('megaphone'),\n title: z.string().trim().min(1, 'Title is required').max(120),\n body: z.string().trim().min(1, 'Message is required').max(250),\n /** Required when `status` is `scheduled`; otherwise sent immediately. */\n scheduledAt: isoDateTimeSchema.optional(),\n status: announcementStatusSchema.default('sent'),\n })\n .refine((v) => v.status !== 'scheduled' || v.scheduledAt != null, {\n message: 'Pick a schedule time',\n path: ['scheduledAt'],\n })\n /**\n * A scheduled time must be in the FUTURE. Without this, yesterday 9am\n * validated cleanly, was stored as `scheduled`, and went out on the next sweep\n * seconds later: the owner saw \"Scheduled\" and the members got it at once.\n *\n * Drafts are exempt on purpose. Nothing is going out, so a stale time on one\n * is not yet a mistake.\n */\n .refine((v) => v.status !== 'scheduled' || !v.scheduledAt || !isScheduleInPast(v.scheduledAt), {\n message: PAST_SCHEDULE_MESSAGE,\n path: ['scheduledAt'],\n });\nexport type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;\n\n/** The create body plus the center it belongs to, as the route receives it. */\nexport const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.and(\n z.object({ centerId: objectIdSchema }),\n);\nexport type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;\n\n// ─── Update body ─────────────────────────────────────────────────────────────\n\n/**\n * Edit a draft, reschedule a queued one, or send either.\n *\n * Every field is optional because this is a PATCH: an owner fixing a typo sends\n * the title alone. `status` is how a draft finally goes out, which is the whole\n * reason this exists — before it there was no update endpoint at all, so \"Save\n * as Draft\" wrote a row that could only ever be deleted.\n *\n * `scheduledAt` accepts null explicitly, meaning \"drop the schedule\", which is\n * different from omitting it (\"leave the schedule alone\"). Conflating the two\n * would make it impossible to turn a scheduled announcement back into a draft.\n */\nexport const updateAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.optional(),\n title: z.string().trim().min(1, 'Title is required').max(120).optional(),\n body: z.string().trim().min(1, 'Message is required').max(250).optional(),\n status: announcementStatusSchema.optional(),\n scheduledAt: isoDateTimeSchema.nullable().optional(),\n});\nexport type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;\n\n// ─── Listing query ───────────────────────────────────────────────────────────\n\n/**\n * How the list is ordered.\n *\n * `newest` is the default because the screen's job is \"what did we just send\".\n * `upcoming` orders by `scheduledAt` ascending and is meaningful only for rows\n * that HAVE one: in a mixed list every draft and every already-sent row sorts as\n * null and groups at the front, so the UI offers it under the Scheduled filter\n * alone.\n */\nexport const announcementSortSchema = z.enum(['newest', 'oldest', 'upcoming']);\nexport type AnnouncementSort = z.infer<typeof announcementSortSchema>;\n\n/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */\nexport const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);\nexport type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;\n\n/**\n * The listing query.\n *\n * Everything arrives as a STRING off the query string, so `paginationSchema`\n * coerces page and limit. Without the coercion `page=2` fails validation and\n * the client gets a 400 for a link it built correctly.\n */\nexport const listAnnouncementsQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema,\n /** Free text over title and body. Trimmed; empty means \"no search\". */\n q: z.string().trim().max(120).optional(),\n status: announcementStatusFilterSchema.default('all'),\n sort: announcementSortSchema.default('newest'),\n});\nexport type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;\n\n/** Just the center, for routes that identify the row by id but still scope it. */\nexport const centerScopeQuerySchema = z.object({ centerId: objectIdSchema });\nexport type CenterScopeQuery = z.infer<typeof centerScopeQuerySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"announcement.js","sourceRoot":"","sources":["../src/announcement.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Owner announcements\n * ====================================\n * Broadcasts an owner sends to a center's members and staff (now or scheduled).\n * Delivered as notifications + push.\n *\n * SINGLE SOURCE OF TRUTH. The listing query, the update body and the\n * past-schedule guard lived in gymmonk-backend and gymmonk-web-client as two\n * hand-kept mirrors before this. They are here so a field can only be added\n * once: zod STRIPS unknown keys, so a mirror that drifts does not fail loudly,\n * it silently drops the field somewhere between the browser and the database.\n *\n * @module gymmonk-schema/announcement\n */\n\nimport { z } from 'zod';\nimport { isoDateTimeSchema, objectIdSchema, paginationSchema } from './common.js';\n\nexport const announcementIconSchema = z.enum(['megaphone', 'warning']);\nexport type AnnouncementIcon = z.infer<typeof announcementIconSchema>;\n\n/**\n * An announcement is a PRESENT EVENT.\n *\n * `scheduled` is gone. A broadcast now goes out the moment it is created, which\n * removes the whole apparatus that existed to make \"later\" work: the due sweep,\n * the claim protocol, the past-date guard, and the Cloud Scheduler job that had\n * to wake a scale-to-zero service every minute for it to mean anything. That\n * job never existed in production, so every scheduled announcement was in fact\n * delivered \"the next time somebody opened the app\" — the feature promised a\n * time and could not keep it.\n *\n * `draft` stays, and is not a contradiction: a draft is not a queued\n * announcement, it is an unsent note. Sending it is still a present event, it\n * just happens when the owner presses the button rather than when they save.\n */\nexport const announcementStatusSchema = z.enum(['draft', 'sent']);\nexport type AnnouncementStatus = z.infer<typeof announcementStatusSchema>;\n\n// ─── Entity ──────────────────────────────────────────────────────────────────\n\nexport const announcementSchema = z.object({\n id: objectIdSchema,\n centerId: objectIdSchema,\n icon: announcementIconSchema,\n title: z.string(),\n body: z.string(),\n status: announcementStatusSchema,\n sentAt: isoDateTimeSchema.nullable(),\n /**\n * How many people it actually reached, once delivery has been attempted.\n *\n * `null` means \"not attempted yet\", which is NOT the same as `0` (\"attempted,\n * nobody to tell\"). An owner broadcasting to an empty roster and an owner\n * whose broadcast has not run yet are different situations, and collapsing\n * both to zero would leave the screen unable to say which.\n */\n deliveredCount: z.number().int().nullable(),\n createdAt: isoDateTimeSchema,\n updatedAt: isoDateTimeSchema,\n});\nexport type Announcement = z.infer<typeof announcementSchema>;\n\n// ─── Create body ─────────────────────────────────────────────────────────────\n\n/**\n * Write one and it goes out, unless it is explicitly saved as a draft.\n *\n * No `scheduledAt`, and no refinements: with delivery immediate there is\n * nothing left to cross-validate. The two that used to be here both existed to\n * police a future time that no longer exists.\n */\nexport const createAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.default('megaphone'),\n title: z.string().trim().min(1, 'Title is required').max(120),\n body: z.string().trim().min(1, 'Message is required').max(250),\n status: announcementStatusSchema.default('sent'),\n});\nexport type CreateAnnouncementBody = z.infer<typeof createAnnouncementBodySchema>;\n\n/** The create body plus the center it belongs to, as the route receives it. */\nexport const createAnnouncementWithCenterSchema = createAnnouncementBodySchema.extend({\n centerId: objectIdSchema,\n});\nexport type CreateAnnouncementWithCenterBody = z.infer<typeof createAnnouncementWithCenterSchema>;\n\n// ─── Update body ─────────────────────────────────────────────────────────────\n\n/**\n * Edit a draft, and send it.\n *\n * Every field is optional because this is a PATCH: an owner fixing a typo sends\n * the title alone. `status: 'sent'` is how a draft finally goes out, which is\n * the whole reason this exists — before it there was no update endpoint at all,\n * so \"Save as Draft\" wrote a row that could only ever be deleted.\n */\nexport const updateAnnouncementBodySchema = z.object({\n icon: announcementIconSchema.optional(),\n title: z.string().trim().min(1, 'Title is required').max(120).optional(),\n body: z.string().trim().min(1, 'Message is required').max(250).optional(),\n status: announcementStatusSchema.optional(),\n});\nexport type UpdateAnnouncementBody = z.infer<typeof updateAnnouncementBodySchema>;\n\n// ─── Listing query ───────────────────────────────────────────────────────────\n\n/**\n * How the list is ordered. `newest` by default, because the screen's job is\n * \"what did we just send\".\n *\n * There was a third, `upcoming`, ordering by the scheduled time. It went with\n * scheduling.\n */\nexport const announcementSortSchema = z.enum(['newest', 'oldest']);\nexport type AnnouncementSort = z.infer<typeof announcementSortSchema>;\n\n/** `all` is a real choice, not an absent filter, so the UI can round-trip it. */\nexport const announcementStatusFilterSchema = z.union([z.literal('all'), announcementStatusSchema]);\nexport type AnnouncementStatusFilter = z.infer<typeof announcementStatusFilterSchema>;\n\n/**\n * The listing query.\n *\n * Everything arrives as a STRING off the query string, so `paginationSchema`\n * coerces page and limit. Without the coercion `page=2` fails validation and\n * the client gets a 400 for a link it built correctly.\n */\nexport const listAnnouncementsQuerySchema = paginationSchema.extend({\n centerId: objectIdSchema,\n /** Free text over title and body. Trimmed; empty means \"no search\". */\n q: z.string().trim().max(120).optional(),\n status: announcementStatusFilterSchema.default('all'),\n sort: announcementSortSchema.default('newest'),\n});\nexport type ListAnnouncementsQuery = z.infer<typeof listAnnouncementsQuerySchema>;\n\n/** Just the center, for routes that identify the row by id but still scope it. */\nexport const centerScopeQuerySchema = z.object({ centerId: objectIdSchema });\nexport type CenterScopeQuery = z.infer<typeof centerScopeQuerySchema>;\n"]}
|
package/dist/area.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"area.d.ts","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,wEAAwE;AACxE,eAAO,MAAM,gBAAgB;;;iBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAE/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;iBAG7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"area.d.ts","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,wEAAwE;AACxE,eAAO,MAAM,gBAAgB;;;iBAG3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;iBAE/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;iBAG7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB;;;;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAa5D,2BAA2B;AAC3B,eAAO,MAAM,oBAAoB;;;iBAG/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,6EAA6E;AAC7E,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,mFAAmF;AACnF,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB;;;;;iBAKhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,iCAAiC;AACjC,eAAO,MAAM,uBAAuB;;iBAElC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC"}
|
package/dist/area.js
CHANGED
|
@@ -64,7 +64,13 @@ export const areaLineageSchema = z.object({
|
|
|
64
64
|
coords: areaCoordsSchema.nullable(),
|
|
65
65
|
});
|
|
66
66
|
// ─── Query params ────────────────────────────────────────────────────────────
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Shared free-text filter. Matched server-side as a case-insensitive SUBSTRING
|
|
69
|
+
* of the area's name — not a prefix, because master data leads a name with its
|
|
70
|
+
* administrative label as often as not ("99-Nakraunda" is the urban ward a
|
|
71
|
+
* person calls Nakraunda), and a prefix match leaves those rows unreachable no
|
|
72
|
+
* matter what the user types.
|
|
73
|
+
*/
|
|
68
74
|
const areaSearchTerm = z.string().trim().max(120).optional();
|
|
69
75
|
/** GET /areas/districts */
|
|
70
76
|
export const districtsQuerySchema = z.object({
|
package/dist/area.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"area.js","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEnE,gFAAgF;AAEhF,wEAAwE;AACxE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;CACjB,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAC1D,IAAI,EAAE,kBAAkB;CACzB,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC;IACpC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;CACrB,CAAC,CAAC;AAGH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,oEAAoE;IACpE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC;IACjC,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAC;AAGH,gFAAgF;AAEhF
|
|
1
|
+
{"version":3,"file":"area.js","sourceRoot":"","sources":["../src/area.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEnE,gFAAgF;AAEhF,wEAAwE;AACxE,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;CACjB,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,MAAM,CAAC;IAC1D,IAAI,EAAE,kBAAkB;CACzB,CAAC,CAAC;AAGH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IACzC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC;IACpC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;CACrB,CAAC,CAAC;AAGH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC;IACxC,oEAAoE;IACpE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC;IACjC,MAAM,EAAE,gBAAgB,CAAC,QAAQ,EAAE;CACpC,CAAC,CAAC;AAGH,gFAAgF;AAEhF;;;;;;GAMG;AACH,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC;AAE7D,2BAA2B;AAC3B,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,OAAO,EAAE,cAAc;IACvB,CAAC,EAAE,cAAc;CAClB,CAAC,CAAC;AAGH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AACrC,mFAAmF;AACnF,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C,UAAU,EAAE,cAAc;IAC1B,CAAC,EAAE,cAAc;IACjB,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC/C,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC;CAC1F,CAAC,CAAC;AAGH,iCAAiC;AACjC,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,MAAM,EAAE,cAAc;CACvB,CAAC,CAAC","sourcesContent":["/**\n * Area lookup API — the contract behind the location picker\n * =========================================================\n * `/api/v1/areas/*` reads reform-owned master data (countries, states,\n * districts, and the eight sub-district types) so a GymMonk address can be\n * PICKED rather than typed.\n *\n * These shapes live here, not in either app, for the reason the whole package\n * exists: the backend and the web client were each declaring their own\n * `AreaOption` and `LocalityOption`, which is two definitions of one wire\n * format and therefore a drift waiting to happen — the backend adding a field\n * or renaming one could not fail the client's build. Declared once, both sides\n * import it and a change breaks loudly at compile time.\n *\n * Everything here is READ-ONLY. Master data is owned by reform-backend;\n * GymMonk never writes to it.\n *\n * @module area\n */\n\nimport { z } from 'zod';\nimport { objectIdSchema } from './common.js';\nimport { areaCoordsSchema, localityTypeSchema } from './shared.js';\n\n// ─── Results ─────────────────────────────────────────────────────────────────\n\n/** One pickable area: the master_db id, and the name to show for it. */\nexport const areaOptionSchema = z.object({\n id: objectIdSchema,\n name: z.string(),\n});\nexport type AreaOption = z.infer<typeof areaOptionSchema>;\n\n/**\n * A locality result also carries its area TYPE.\n *\n * That is not decoration. One district holds rows of eight different types, and\n * names repeat across them — reform's master data has \"Nakraunda\" as both a\n * Village and a Hamlet under Dehradun. Merged into one alphabetical list, the\n * type is the only thing that tells the two apart.\n */\nexport const localityOptionSchema = areaOptionSchema.extend({\n type: localityTypeSchema,\n});\nexport type LocalityOption = z.infer<typeof localityOptionSchema>;\n\n/**\n * One page of localities.\n *\n * `hasMore` rather than a total count: a large district holds thousands of\n * rows, and an infinite-scrolling list only ever needs to know whether to keep\n * going. The server derives it by fetching one row beyond the page, which\n * avoids a second `countDocuments` over the same filter.\n */\nexport const localityPageSchema = z.object({\n items: z.array(localityOptionSchema),\n hasMore: z.boolean(),\n});\nexport type LocalityPage = z.infer<typeof localityPageSchema>;\n\n/**\n * Ancestor ids and coordinates for one area, resolved once when an address is\n * saved and then denormalized onto it — so reading an address never touches\n * master data again.\n *\n * `coords` is nullable, not optional: \"we looked and this chain has no\n * coordinates\" is a real answer, and collapsing it to a missing field would\n * make it indistinguishable from a response that failed to include it.\n */\nexport const areaLineageSchema = z.object({\n /** Every ancestor id, leaf → country, including the area itself. */\n ancestry: z.array(objectIdSchema),\n coords: areaCoordsSchema.nullable(),\n});\nexport type AreaLineage = z.infer<typeof areaLineageSchema>;\n\n// ─── Query params ────────────────────────────────────────────────────────────\n\n/**\n * Shared free-text filter. Matched server-side as a case-insensitive SUBSTRING\n * of the area's name — not a prefix, because master data leads a name with its\n * administrative label as often as not (\"99-Nakraunda\" is the urban ward a\n * person calls Nakraunda), and a prefix match leaves those rows unreachable no\n * matter what the user types.\n */\nconst areaSearchTerm = z.string().trim().max(120).optional();\n\n/** GET /areas/districts */\nexport const districtsQuerySchema = z.object({\n stateId: objectIdSchema,\n q: areaSearchTerm,\n});\nexport type DistrictsQuery = z.infer<typeof districtsQuerySchema>;\n\n/** Rows per locality page. Mirrored by the client's infinite-scroll hook. */\nexport const LOCALITY_PAGE_SIZE = 50;\n/** Ceiling on a caller-supplied `limit`, so one request cannot pull a district. */\nexport const LOCALITY_MAX_LIMIT = 100;\n\n/**\n * GET /areas/localities\n *\n * `page`/`limit` are coerced because they arrive as query strings, and clamped\n * here rather than in the handler so the frontend sees the same bounds it will\n * be held to.\n */\nexport const localitiesQuerySchema = z.object({\n districtId: objectIdSchema,\n q: areaSearchTerm,\n page: z.coerce.number().int().min(1).default(1),\n limit: z.coerce.number().int().min(1).max(LOCALITY_MAX_LIMIT).default(LOCALITY_PAGE_SIZE),\n});\nexport type LocalitiesQuery = z.infer<typeof localitiesQuerySchema>;\n\n/** GET /areas/lineage/:areaId */\nexport const areaLineageParamsSchema = z.object({\n areaId: objectIdSchema,\n});\nexport type AreaLineageParams = z.infer<typeof areaLineageParamsSchema>;\n"]}
|
package/dist/session.d.ts
CHANGED
|
@@ -130,6 +130,32 @@ export declare const attendanceStatusSchema: z.ZodEnum<{
|
|
|
130
130
|
rest: "rest";
|
|
131
131
|
}>;
|
|
132
132
|
export type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;
|
|
133
|
+
/**
|
|
134
|
+
* How an attendance mark came to be recorded.
|
|
135
|
+
*
|
|
136
|
+
* `scan` — the member scanned the gym's QR poster themselves.
|
|
137
|
+
* `manual` — a person put it there: an owner, manager or front desk.
|
|
138
|
+
* `auto` — nobody did. The member never scanned out and the session hit its
|
|
139
|
+
* cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).
|
|
140
|
+
*
|
|
141
|
+
* Recorded per SIDE, because the two halves of a visit routinely differ: the
|
|
142
|
+
* common case is a member who scans in and is closed out automatically. An
|
|
143
|
+
* attendance report that cannot tell a scanned departure from an assumed one is
|
|
144
|
+
* reporting a guess as a measurement.
|
|
145
|
+
*/
|
|
146
|
+
export declare const attendanceSourceSchema: z.ZodEnum<{
|
|
147
|
+
scan: "scan";
|
|
148
|
+
manual: "manual";
|
|
149
|
+
auto: "auto";
|
|
150
|
+
}>;
|
|
151
|
+
export type AttendanceSource = z.infer<typeof attendanceSourceSchema>;
|
|
152
|
+
/**
|
|
153
|
+
* How long a session may stay open before it is closed for the member.
|
|
154
|
+
*
|
|
155
|
+
* Shared with the client so the member's screen can say when their session will
|
|
156
|
+
* end, rather than having the number live only in the server that enforces it.
|
|
157
|
+
*/
|
|
158
|
+
export declare const AUTO_CHECK_OUT_HOURS = 3;
|
|
133
159
|
export declare const attendanceEntrySchema: z.ZodObject<{
|
|
134
160
|
date: z.ZodString;
|
|
135
161
|
checkInAt: z.ZodNullable<z.ZodISODateTime>;
|
|
@@ -139,6 +165,18 @@ export declare const attendanceEntrySchema: z.ZodObject<{
|
|
|
139
165
|
absent: "absent";
|
|
140
166
|
rest: "rest";
|
|
141
167
|
}>;
|
|
168
|
+
checkInSource: z.ZodNullable<z.ZodEnum<{
|
|
169
|
+
scan: "scan";
|
|
170
|
+
manual: "manual";
|
|
171
|
+
auto: "auto";
|
|
172
|
+
}>>;
|
|
173
|
+
checkOutSource: z.ZodNullable<z.ZodEnum<{
|
|
174
|
+
scan: "scan";
|
|
175
|
+
manual: "manual";
|
|
176
|
+
auto: "auto";
|
|
177
|
+
}>>;
|
|
178
|
+
checkInBy: z.ZodNullable<z.ZodString>;
|
|
179
|
+
checkOutBy: z.ZodNullable<z.ZodString>;
|
|
142
180
|
}, z.core.$strip>;
|
|
143
181
|
export type AttendanceEntry = z.infer<typeof attendanceEntrySchema>;
|
|
144
182
|
export declare const attendanceQuerySchema: z.ZodObject<{
|
package/dist/session.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,mBAAmB;;;;EAA0C,CAAC;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,gBAI5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAO/F;AAID,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAOQ,CAAC;AACvC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB;;;;;;iBAEpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E,eAAO,MAAM,sBAAsB;;;;EAAwC,CAAC;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,eAAO,MAAM,qBAAqB
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAcxB,eAAO,MAAM,mBAAmB;;;;EAA0C,CAAC;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAIhE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,sBAAsB;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,+CAA+C;AAC/C,eAAO,MAAM,cAAc,EAAE,gBAI5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,gBAAgB,EAAE,GAAG,GAAE,IAAiB,GAAG,MAAM,CAO/F;AAID,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAY/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAIlE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB;;;;;iBAOQ,CAAC;AACvC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;iBAE7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,0BAA0B;;;iBAGrC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB;;;;;;iBAEpC,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAI5E,eAAO,MAAM,sBAAsB;;;;EAAwC,CAAC;AAC5E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,sBAAsB;;;;EAAqC,CAAC;AACzE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;iBAkBhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,eAAO,MAAM,qBAAqB;;;;;;iBAIhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,sEAAsE;AACtE,eAAO,MAAM,wBAAwB;;;;;;;;;;;iBAOnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC"}
|
package/dist/session.js
CHANGED
|
@@ -121,11 +121,45 @@ export const stopwatchActionBodySchema = z.object({
|
|
|
121
121
|
});
|
|
122
122
|
// ─── Attendance ──────────────────────────────────────────────────────────────
|
|
123
123
|
export const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);
|
|
124
|
+
/**
|
|
125
|
+
* How an attendance mark came to be recorded.
|
|
126
|
+
*
|
|
127
|
+
* `scan` — the member scanned the gym's QR poster themselves.
|
|
128
|
+
* `manual` — a person put it there: an owner, manager or front desk.
|
|
129
|
+
* `auto` — nobody did. The member never scanned out and the session hit its
|
|
130
|
+
* cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).
|
|
131
|
+
*
|
|
132
|
+
* Recorded per SIDE, because the two halves of a visit routinely differ: the
|
|
133
|
+
* common case is a member who scans in and is closed out automatically. An
|
|
134
|
+
* attendance report that cannot tell a scanned departure from an assumed one is
|
|
135
|
+
* reporting a guess as a measurement.
|
|
136
|
+
*/
|
|
137
|
+
export const attendanceSourceSchema = z.enum(['scan', 'manual', 'auto']);
|
|
138
|
+
/**
|
|
139
|
+
* How long a session may stay open before it is closed for the member.
|
|
140
|
+
*
|
|
141
|
+
* Shared with the client so the member's screen can say when their session will
|
|
142
|
+
* end, rather than having the number live only in the server that enforces it.
|
|
143
|
+
*/
|
|
144
|
+
export const AUTO_CHECK_OUT_HOURS = 3;
|
|
124
145
|
export const attendanceEntrySchema = z.object({
|
|
125
146
|
date: isoDateSchema,
|
|
126
147
|
checkInAt: isoDateTimeSchema.nullable(),
|
|
127
148
|
checkOutAt: isoDateTimeSchema.nullable(),
|
|
128
149
|
status: attendanceStatusSchema,
|
|
150
|
+
/** How each half was recorded. Null where that half has not happened. */
|
|
151
|
+
checkInSource: attendanceSourceSchema.nullable(),
|
|
152
|
+
checkOutSource: attendanceSourceSchema.nullable(),
|
|
153
|
+
/**
|
|
154
|
+
* WHO marked it, when a person did — a name, snapshotted at the time.
|
|
155
|
+
*
|
|
156
|
+
* Null for `scan` and `auto`, which have no author. A snapshot rather than a
|
|
157
|
+
* live lookup because this is a record of what happened: a manager who later
|
|
158
|
+
* leaves, or changes their name, must not silently rewrite last month's
|
|
159
|
+
* attendance sheet.
|
|
160
|
+
*/
|
|
161
|
+
checkInBy: z.string().nullable(),
|
|
162
|
+
checkOutBy: z.string().nullable(),
|
|
129
163
|
});
|
|
130
164
|
export const attendanceQuerySchema = paginationSchema.extend({
|
|
131
165
|
userId: objectIdSchema.optional(),
|
package/dist/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Workout sessions, check-in & attendance\n * ========================================================\n * A member checks in to start a live session, marks exercises done, and checks\n * out. Sessions roll up into attendance records the owner can review, and can\n * also be recorded directly by an owner (manual attendance marking).\n *\n * @module gymmonk-schema/session\n */\n\nimport { z } from 'zod';\nimport { checkInCodeSchema } from './check-in.js';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n} from './common.js';\nimport { checkInGeoInputSchema } from './location.js';\nimport { weekdaySchema } from './shared.js';\n\n// ─── Live session widget status ──────────────────────────────────────────────\n\nexport const sessionStatusSchema = z.enum(['idle', 'active', 'completed']);\nexport type SessionStatus = z.infer<typeof sessionStatusSchema>;\n\n// ─── The set stopwatch ───────────────────────────────────────────────────────\n\n/**\n * The member's own stopwatch, which is NOT the session clock.\n *\n * Home shows two running times and they measure different things. The session\n * clock is \"how long have you been in the gym\" — it runs from `checkInAt`,\n * cannot be paused, and is derived rather than stored. This one is \"how long\n * has this set taken\", and the member starts, pauses and resets it freely.\n *\n * Stored as ACCUMULATED + STARTED-AT rather than as a running total, because a\n * total would need the server to tick. Elapsed is\n * `accumulatedSec + (running ? now - startedAt : 0)`, so a paused stopwatch is\n * a plain number, a running one survives a refresh or a dead battery, and no\n * job has to write to the database once a second.\n *\n * `startedAt` is null exactly when `running` is false; the pair is a small\n * state machine, and the backend is the only writer.\n */\nexport const sessionStopwatchSchema = z.object({\n running: z.boolean(),\n /** Seconds banked by previous runs, excluding any run in progress. */\n accumulatedSec: z.number().int().nonnegative(),\n /** When the current run began. Null while paused. */\n startedAt: isoDateTimeSchema.nullable(),\n});\nexport type SessionStopwatch = z.infer<typeof sessionStopwatchSchema>;\n\n/** A stopwatch that has never been started. */\nexport const IDLE_STOPWATCH: SessionStopwatch = {\n running: false,\n accumulatedSec: 0,\n startedAt: null,\n};\n\n/**\n * Resolve a stopwatch to whole seconds elapsed.\n *\n * Shared rather than reimplemented on each side: the backend needs it to bank\n * time on pause, and the client needs it to render every tick. Two copies of\n * this arithmetic would drift the moment one of them forgot the running run.\n */\nexport function stopwatchElapsedSec(stopwatch: SessionStopwatch, now: Date = new Date()): number {\n if (!stopwatch.running || !stopwatch.startedAt) return stopwatch.accumulatedSec;\n const started = Date.parse(stopwatch.startedAt);\n if (Number.isNaN(started)) return stopwatch.accumulatedSec;\n // Floored at the banked value: a client clock behind the server's would\n // otherwise make the number run backwards.\n return stopwatch.accumulatedSec + Math.max(0, Math.floor((now.getTime() - started) / 1000));\n}\n\n// ─── Session entity ──────────────────────────────────────────────────────────\n\nexport const workoutSessionSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema,\n centerId: objectIdSchema,\n status: sessionStatusSchema,\n checkInAt: isoDateTimeSchema,\n checkOutAt: isoDateTimeSchema.nullable(),\n /** The plan weekday being trained, if the member is on a plan. */\n planWeekday: weekdaySchema.nullable(),\n completedExerciseIds: z.array(objectIdSchema),\n totalExercises: z.number().int().nonnegative(),\n stopwatch: sessionStopwatchSchema,\n});\nexport type WorkoutSession = z.infer<typeof workoutSessionSchema>;\n\n// ─── Session mutations ───────────────────────────────────────────────────────\n\n/**\n * Check in by scanning the gym's QR poster.\n *\n * `code` is REQUIRED and is the whole point: the center is resolved FROM the\n * scanned code, never from a client-supplied `centerId`. An earlier version of\n * this body took an optional `centerId`, which meant any authenticated member\n * could mark themselves present from their sofa. Presence has to be proven by\n * being close enough to the poster to photograph it.\n *\n * Staff marking someone present by hand is a different, authorised path —\n * `markAttendanceBodySchema` below.\n */\nexport const checkInBodySchema = z\n .object({\n code: checkInCodeSchema,\n })\n // Where the scanning device thought it was. All three optional — see\n // `checkInGeoInputSchema`. The server measures this against the gym's pin and\n // treats a missing fix as unverifiable rather than as a failure.\n .extend(checkInGeoInputSchema.shape);\nexport type CheckInBody = z.infer<typeof checkInBodySchema>;\n\n/**\n * Check out — the same poster, scanned again. Verified against the session's\n * OWN center, so a member cannot close a session by scanning a different gym.\n */\nexport const checkOutBodySchema = z.object({\n code: checkInCodeSchema,\n});\nexport type CheckOutBody = z.infer<typeof checkOutBodySchema>;\n\nexport const markExerciseDoneBodySchema = z.object({\n exerciseId: objectIdSchema,\n done: z.boolean(),\n});\nexport type MarkExerciseDoneBody = z.infer<typeof markExerciseDoneBodySchema>;\n\n/**\n * Drive the set stopwatch.\n *\n * An ACTION, not a state patch. The client says what the member pressed and the\n * server computes the resulting `{ running, accumulatedSec, startedAt }` — which\n * keeps the banking arithmetic in one place and means a client cannot post an\n * arbitrary elapsed time to inflate a workout.\n */\nexport const stopwatchActionBodySchema = z.object({\n action: z.enum(['start', 'pause', 'reset']),\n});\nexport type StopwatchActionBody = z.infer<typeof stopwatchActionBodySchema>;\n\n// ─── Attendance ──────────────────────────────────────────────────────────────\n\nexport const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);\nexport type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;\n\nexport const attendanceEntrySchema = z.object({\n date: isoDateSchema,\n checkInAt: isoDateTimeSchema.nullable(),\n checkOutAt: isoDateTimeSchema.nullable(),\n status: attendanceStatusSchema,\n});\nexport type AttendanceEntry = z.infer<typeof attendanceEntrySchema>;\n\nexport const attendanceQuerySchema = paginationSchema.extend({\n userId: objectIdSchema.optional(),\n from: isoDateSchema.optional(),\n to: isoDateSchema.optional(),\n});\nexport type AttendanceQuery = z.infer<typeof attendanceQuerySchema>;\n\n/** Owner marking attendance for a member or staff member manually. */\nexport const markAttendanceBodySchema = z.object({\n userId: objectIdSchema,\n date: isoDateSchema.optional(),\n status: attendanceStatusSchema.default('present'),\n checkInAt: isoDateTimeSchema.optional(),\n checkOutAt: isoDateTimeSchema.optional(),\n note: optionalTrimmedString(160),\n});\nexport type MarkAttendanceBody = z.infer<typeof markAttendanceBodySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * gymmonk-schema — Workout sessions, check-in & attendance\n * ========================================================\n * A member checks in to start a live session, marks exercises done, and checks\n * out. Sessions roll up into attendance records the owner can review, and can\n * also be recorded directly by an owner (manual attendance marking).\n *\n * @module gymmonk-schema/session\n */\n\nimport { z } from 'zod';\nimport { checkInCodeSchema } from './check-in.js';\nimport {\n isoDateSchema,\n isoDateTimeSchema,\n objectIdSchema,\n optionalTrimmedString,\n paginationSchema,\n} from './common.js';\nimport { checkInGeoInputSchema } from './location.js';\nimport { weekdaySchema } from './shared.js';\n\n// ─── Live session widget status ──────────────────────────────────────────────\n\nexport const sessionStatusSchema = z.enum(['idle', 'active', 'completed']);\nexport type SessionStatus = z.infer<typeof sessionStatusSchema>;\n\n// ─── The set stopwatch ───────────────────────────────────────────────────────\n\n/**\n * The member's own stopwatch, which is NOT the session clock.\n *\n * Home shows two running times and they measure different things. The session\n * clock is \"how long have you been in the gym\" — it runs from `checkInAt`,\n * cannot be paused, and is derived rather than stored. This one is \"how long\n * has this set taken\", and the member starts, pauses and resets it freely.\n *\n * Stored as ACCUMULATED + STARTED-AT rather than as a running total, because a\n * total would need the server to tick. Elapsed is\n * `accumulatedSec + (running ? now - startedAt : 0)`, so a paused stopwatch is\n * a plain number, a running one survives a refresh or a dead battery, and no\n * job has to write to the database once a second.\n *\n * `startedAt` is null exactly when `running` is false; the pair is a small\n * state machine, and the backend is the only writer.\n */\nexport const sessionStopwatchSchema = z.object({\n running: z.boolean(),\n /** Seconds banked by previous runs, excluding any run in progress. */\n accumulatedSec: z.number().int().nonnegative(),\n /** When the current run began. Null while paused. */\n startedAt: isoDateTimeSchema.nullable(),\n});\nexport type SessionStopwatch = z.infer<typeof sessionStopwatchSchema>;\n\n/** A stopwatch that has never been started. */\nexport const IDLE_STOPWATCH: SessionStopwatch = {\n running: false,\n accumulatedSec: 0,\n startedAt: null,\n};\n\n/**\n * Resolve a stopwatch to whole seconds elapsed.\n *\n * Shared rather than reimplemented on each side: the backend needs it to bank\n * time on pause, and the client needs it to render every tick. Two copies of\n * this arithmetic would drift the moment one of them forgot the running run.\n */\nexport function stopwatchElapsedSec(stopwatch: SessionStopwatch, now: Date = new Date()): number {\n if (!stopwatch.running || !stopwatch.startedAt) return stopwatch.accumulatedSec;\n const started = Date.parse(stopwatch.startedAt);\n if (Number.isNaN(started)) return stopwatch.accumulatedSec;\n // Floored at the banked value: a client clock behind the server's would\n // otherwise make the number run backwards.\n return stopwatch.accumulatedSec + Math.max(0, Math.floor((now.getTime() - started) / 1000));\n}\n\n// ─── Session entity ──────────────────────────────────────────────────────────\n\nexport const workoutSessionSchema = z.object({\n id: objectIdSchema,\n userId: objectIdSchema,\n centerId: objectIdSchema,\n status: sessionStatusSchema,\n checkInAt: isoDateTimeSchema,\n checkOutAt: isoDateTimeSchema.nullable(),\n /** The plan weekday being trained, if the member is on a plan. */\n planWeekday: weekdaySchema.nullable(),\n completedExerciseIds: z.array(objectIdSchema),\n totalExercises: z.number().int().nonnegative(),\n stopwatch: sessionStopwatchSchema,\n});\nexport type WorkoutSession = z.infer<typeof workoutSessionSchema>;\n\n// ─── Session mutations ───────────────────────────────────────────────────────\n\n/**\n * Check in by scanning the gym's QR poster.\n *\n * `code` is REQUIRED and is the whole point: the center is resolved FROM the\n * scanned code, never from a client-supplied `centerId`. An earlier version of\n * this body took an optional `centerId`, which meant any authenticated member\n * could mark themselves present from their sofa. Presence has to be proven by\n * being close enough to the poster to photograph it.\n *\n * Staff marking someone present by hand is a different, authorised path —\n * `markAttendanceBodySchema` below.\n */\nexport const checkInBodySchema = z\n .object({\n code: checkInCodeSchema,\n })\n // Where the scanning device thought it was. All three optional — see\n // `checkInGeoInputSchema`. The server measures this against the gym's pin and\n // treats a missing fix as unverifiable rather than as a failure.\n .extend(checkInGeoInputSchema.shape);\nexport type CheckInBody = z.infer<typeof checkInBodySchema>;\n\n/**\n * Check out — the same poster, scanned again. Verified against the session's\n * OWN center, so a member cannot close a session by scanning a different gym.\n */\nexport const checkOutBodySchema = z.object({\n code: checkInCodeSchema,\n});\nexport type CheckOutBody = z.infer<typeof checkOutBodySchema>;\n\nexport const markExerciseDoneBodySchema = z.object({\n exerciseId: objectIdSchema,\n done: z.boolean(),\n});\nexport type MarkExerciseDoneBody = z.infer<typeof markExerciseDoneBodySchema>;\n\n/**\n * Drive the set stopwatch.\n *\n * An ACTION, not a state patch. The client says what the member pressed and the\n * server computes the resulting `{ running, accumulatedSec, startedAt }` — which\n * keeps the banking arithmetic in one place and means a client cannot post an\n * arbitrary elapsed time to inflate a workout.\n */\nexport const stopwatchActionBodySchema = z.object({\n action: z.enum(['start', 'pause', 'reset']),\n});\nexport type StopwatchActionBody = z.infer<typeof stopwatchActionBodySchema>;\n\n// ─── Attendance ──────────────────────────────────────────────────────────────\n\nexport const attendanceStatusSchema = z.enum(['present', 'absent', 'rest']);\nexport type AttendanceStatus = z.infer<typeof attendanceStatusSchema>;\n\n/**\n * How an attendance mark came to be recorded.\n *\n * `scan` — the member scanned the gym's QR poster themselves.\n * `manual` — a person put it there: an owner, manager or front desk.\n * `auto` — nobody did. The member never scanned out and the session hit its\n * cap, so the system closed it (see `AUTO_CHECK_OUT_HOURS`).\n *\n * Recorded per SIDE, because the two halves of a visit routinely differ: the\n * common case is a member who scans in and is closed out automatically. An\n * attendance report that cannot tell a scanned departure from an assumed one is\n * reporting a guess as a measurement.\n */\nexport const attendanceSourceSchema = z.enum(['scan', 'manual', 'auto']);\nexport type AttendanceSource = z.infer<typeof attendanceSourceSchema>;\n\n/**\n * How long a session may stay open before it is closed for the member.\n *\n * Shared with the client so the member's screen can say when their session will\n * end, rather than having the number live only in the server that enforces it.\n */\nexport const AUTO_CHECK_OUT_HOURS = 3;\n\nexport const attendanceEntrySchema = z.object({\n date: isoDateSchema,\n checkInAt: isoDateTimeSchema.nullable(),\n checkOutAt: isoDateTimeSchema.nullable(),\n status: attendanceStatusSchema,\n /** How each half was recorded. Null where that half has not happened. */\n checkInSource: attendanceSourceSchema.nullable(),\n checkOutSource: attendanceSourceSchema.nullable(),\n /**\n * WHO marked it, when a person did — a name, snapshotted at the time.\n *\n * Null for `scan` and `auto`, which have no author. A snapshot rather than a\n * live lookup because this is a record of what happened: a manager who later\n * leaves, or changes their name, must not silently rewrite last month's\n * attendance sheet.\n */\n checkInBy: z.string().nullable(),\n checkOutBy: z.string().nullable(),\n});\nexport type AttendanceEntry = z.infer<typeof attendanceEntrySchema>;\n\nexport const attendanceQuerySchema = paginationSchema.extend({\n userId: objectIdSchema.optional(),\n from: isoDateSchema.optional(),\n to: isoDateSchema.optional(),\n});\nexport type AttendanceQuery = z.infer<typeof attendanceQuerySchema>;\n\n/** Owner marking attendance for a member or staff member manually. */\nexport const markAttendanceBodySchema = z.object({\n userId: objectIdSchema,\n date: isoDateSchema.optional(),\n status: attendanceStatusSchema.default('present'),\n checkInAt: isoDateTimeSchema.optional(),\n checkOutAt: isoDateTimeSchema.optional(),\n note: optionalTrimmedString(160),\n});\nexport type MarkAttendanceBody = z.infer<typeof markAttendanceBodySchema>;\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gymmonk-schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|