hvp-shared 13.26.0 → 13.35.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.
@@ -166,6 +166,79 @@ export declare const SCHEDULER_WITH_DATE_REGEX: RegExp;
166
166
  * date.
167
167
  */
168
168
  export declare const SCHEDULER_DATE_FALLBACK_REGEX: RegExp;
169
+ /**
170
+ * Version tag stored in `extendedProperties.private.descriptionFormat` on
171
+ * events created with the labeled format. Lets analytics / future migration
172
+ * scripts distinguish labeled from positional events.
173
+ */
174
+ export declare const LABELED_DESCRIPTION_FORMAT_VERSION = "labeled-v1";
175
+ /**
176
+ * Per-field spec for the labeled description format.
177
+ *
178
+ * Each field has:
179
+ * - `label`: canonical label written when building (e.g., "Tutor").
180
+ * - `regex`: extraction regex with capturing groups. Group 1 = value.
181
+ * For `tutor`, group 2 captures the optional QVET id digits.
182
+ *
183
+ * Order-independent: labels anchor each field, so the format tolerates
184
+ * reordering or manual edits as long as the labels are present.
185
+ *
186
+ * Tolerates: `Tutor:`, `tutor :`, `TUTOR: `, trailing whitespace.
187
+ */
188
+ export declare const LABELED_DESCRIPTION_FIELDS: {
189
+ readonly tutor: {
190
+ readonly label: "Tutor";
191
+ readonly regex: RegExp;
192
+ };
193
+ readonly phones: {
194
+ readonly label: "Teléfonos";
195
+ readonly regex: RegExp;
196
+ };
197
+ readonly pet: {
198
+ readonly label: "Mascota";
199
+ readonly regex: RegExp;
200
+ };
201
+ readonly breed: {
202
+ readonly label: "Raza";
203
+ readonly regex: RegExp;
204
+ };
205
+ readonly age: {
206
+ readonly label: "Edad";
207
+ readonly regex: RegExp;
208
+ };
209
+ readonly motivo: {
210
+ readonly label: "Motivo";
211
+ readonly regex: RegExp;
212
+ };
213
+ readonly discount: {
214
+ readonly label: "Descuento";
215
+ readonly regex: RegExp;
216
+ };
217
+ };
218
+ export type LabeledDescriptionField = keyof typeof LABELED_DESCRIPTION_FIELDS;
219
+ /**
220
+ * Returns "labeled" if any known field label is present at the start of a line
221
+ * in the description; otherwise "positional" (legacy format).
222
+ */
223
+ export declare function detectDescriptionFormat(description: string): "labeled" | "positional";
224
+ /**
225
+ * Extract a single-line labeled field. Returns trimmed value or null.
226
+ *
227
+ * For `motivo` (which may span multiple lines), use {@link extractMotivoLabeled}.
228
+ * For the tutor's QVET id, use {@link extractTutorQvetIdLabeled}.
229
+ */
230
+ export declare function extractLabeledField(description: string, field: Exclude<LabeledDescriptionField, "motivo">): string | null;
231
+ /**
232
+ * Extract the QVET client id from the `Tutor: NAME (Q12345)` line. Returns the
233
+ * numeric portion as a string (e.g., "12345") or null.
234
+ */
235
+ export declare function extractTutorQvetIdLabeled(description: string): string | null;
236
+ /**
237
+ * Extract the motivo block which may span multiple lines. Starts at the
238
+ * `Motivo:` line and collects subsequent lines until another labeled field,
239
+ * an AGENDÓ/REAGENDÓ line, the HVP marker, or end of description.
240
+ */
241
+ export declare function extractMotivoLabeled(description: string): string | null;
169
242
  /**
170
243
  * Captures pet age expressions in Spanish from descriptions.
171
244
  *
@@ -197,4 +270,11 @@ export declare const HVP_EVENT_PROP_KEYS: {
197
270
  readonly isPreferredVet: "isPreferredVet";
198
271
  readonly source: "source";
199
272
  readonly standardVersion: "standardVersion";
273
+ /**
274
+ * Marks the description-builder format version used at create/update time
275
+ * (e.g., "labeled-v1"). Informational only — the parser detects format from
276
+ * content, not from this prop. Useful for analytics ("% of events on labeled
277
+ * format") and future migration scripts.
278
+ */
279
+ readonly descriptionFormat: "descriptionFormat";
200
280
  };
@@ -13,7 +13,11 @@
13
13
  * runs remain reproducible.
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.HVP_EVENT_PROP_KEYS = exports.KNOWN_BREEDS = exports.PET_AGE_REGEX = exports.SCHEDULER_DATE_FALLBACK_REGEX = exports.SCHEDULER_WITH_DATE_REGEX = exports.SCHEDULER_REGEX = exports.PHONE_REGEX = exports.QVET_ID_REGEX = exports.CANCELLED_TITLE_REGEX = exports.REMINDER_TITLE_REGEX = exports.PREFERRED_VET_REGEX = exports.EXCLUDED_TITLE_PREFIXES = exports.SERVICE_CODE_SYNONYMS = exports.CANONICAL_SERVICE_CODES = exports.EXCLUDED_COLOR_IDS = exports.NO_SHOW_COLOR_ID = exports.CANCELLED_COLOR_ID = exports.COLOR_ID_TO_BRANCH = exports.BRANCH_COLOR_IDS = exports.STANDARD_V1 = exports.STANDARD_V3 = exports.STANDARD_V2 = void 0;
16
+ exports.HVP_EVENT_PROP_KEYS = exports.KNOWN_BREEDS = exports.PET_AGE_REGEX = exports.LABELED_DESCRIPTION_FIELDS = exports.LABELED_DESCRIPTION_FORMAT_VERSION = exports.SCHEDULER_DATE_FALLBACK_REGEX = exports.SCHEDULER_WITH_DATE_REGEX = exports.SCHEDULER_REGEX = exports.PHONE_REGEX = exports.QVET_ID_REGEX = exports.CANCELLED_TITLE_REGEX = exports.REMINDER_TITLE_REGEX = exports.PREFERRED_VET_REGEX = exports.EXCLUDED_TITLE_PREFIXES = exports.SERVICE_CODE_SYNONYMS = exports.CANONICAL_SERVICE_CODES = exports.EXCLUDED_COLOR_IDS = exports.NO_SHOW_COLOR_ID = exports.CANCELLED_COLOR_ID = exports.COLOR_ID_TO_BRANCH = exports.BRANCH_COLOR_IDS = exports.STANDARD_V1 = exports.STANDARD_V3 = exports.STANDARD_V2 = void 0;
17
+ exports.detectDescriptionFormat = detectDescriptionFormat;
18
+ exports.extractLabeledField = extractLabeledField;
19
+ exports.extractTutorQvetIdLabeled = extractTutorQvetIdLabeled;
20
+ exports.extractMotivoLabeled = extractMotivoLabeled;
17
21
  // ─── Standard v1 ─────────────────────────────────────────────────────────────
18
22
  /**
19
23
  * Multi-factor appointment standard v2.
@@ -147,31 +151,31 @@ exports.STANDARD_V3 = {
147
151
  id: "context",
148
152
  weight: 8,
149
153
  label: "Motivo / contexto",
150
- description: 'La descripción tiene contexto clínico relevante (motivo de la visita, síntomas, antecedentes) más allá del nombre y teléfono. Mínimo 20 caracteres tras descontar nombre, teléfono y Q{id}.',
154
+ description: 'Eventos en formato labeled: cuenta el bloque "Motivo:" (>20 caracteres). Eventos legacy posicionales: la descripción tiene contexto clínico relevante más allá del nombre y teléfono mínimo 20 caracteres tras descontar nombre, teléfono y Q{id}.',
151
155
  },
152
156
  {
153
157
  id: "owner_name",
154
158
  weight: 8,
155
159
  label: "Nombre del tutor",
156
- description: 'Nombre del cliente/tutor. Auto-pasa si hay Q{id} en la descripción (cliente linkeado a QVET) o si el teléfono matchea un cliente QVET existente. Si no, busca nombre completo del tutor en la descripción (mínimo 2 palabras, mayúsculas/minúsculas con letras).',
160
+ description: 'Nombre del cliente/tutor. Auto-pasa si hay Q{id} en la descripción (cliente linkeado a QVET) o si el teléfono matchea un cliente QVET existente. Eventos labeled: lo lee de la línea "Tutor:". Eventos legacy: lo busca en la primera línea con 2+ palabras de letras (o el patrón "NAME | PETS | Q{id}").',
157
161
  },
158
162
  {
159
163
  id: "owner_phone",
160
164
  weight: 8,
161
165
  label: "Teléfono del tutor",
162
- description: 'Teléfono del tutor en cualquier formato común (con o sin +52, con o sin separadores). Auto-pasa si hay Q{id} (sabemos el teléfono del registro QVET). Necesario para confirmar la cita y avisar de cambios.',
166
+ description: 'Teléfono del tutor en cualquier formato común (con o sin +52, con o sin separadores). Eventos labeled: lo lee de la línea "Teléfonos:". Eventos legacy: lo busca en cualquier línea de la descripción. Auto-pasa si hay Q{id} (sabemos el teléfono del registro QVET).',
163
167
  },
164
168
  {
165
169
  id: "pet_age",
166
170
  weight: 4,
167
171
  label: "Edad de la mascota",
168
- description: 'Edad anotada en la descripción: "4 meses", "1 año y 4 meses", "8 semanas", "2 años". Importante para vacunación, dosis y diagnóstico.',
172
+ description: 'Edad anotada en la descripción. Eventos labeled: línea "Edad:". Eventos legacy: detección por patrón ("4 meses", "1 año y 4 meses", "8 semanas", "2 años"). Importante para vacunación, dosis y diagnóstico.',
169
173
  },
170
174
  {
171
175
  id: "pet_breed",
172
176
  weight: 4,
173
177
  label: "Raza de la mascota",
174
- description: 'Raza detectada en la descripción contra un diccionario de razas comunes: Yorkie, Shih Tzu, Cocker Spaniel, French Bulldog, Chihuahua, Schnauzer, Golden Retriever, Labrador, Persa, Siamés, mestizo, etc. Útil para protocolos clínicos y dosificación.',
178
+ description: 'Raza detectada en la descripción. Eventos labeled: línea "Raza:". Eventos legacy: match contra un diccionario de razas comunes (Yorkie, Shih Tzu, Cocker Spaniel, French Bulldog, Chihuahua, Schnauzer, Golden Retriever, Labrador, Persa, Siamés, mestizo, etc.).',
175
179
  },
176
180
  {
177
181
  id: "discount_format",
@@ -433,6 +437,122 @@ exports.SCHEDULER_WITH_DATE_REGEX = /\b(?:AG[.:]?|AGE[.:]|AGEND[OÓ][.:]?|AGENG[
433
437
  * date.
434
438
  */
435
439
  exports.SCHEDULER_DATE_FALLBACK_REGEX = /\b(\d{1,2}[./\-]\d{1,2}(?:[./\-]\d{2,4})?|\d{1,2}[./\-](?:ENE|FEB|MAR|ABR|MAY|JUN|JUL|AGO|SEP|OCT|NOV|DIC|ENERO|FEBRERO|MARZO|ABRIL|MAYO|JUNIO|JULIO|AGOSTO|SEPTIEMBRE|OCTUBRE|NOVIEMBRE|DICIEMBRE)(?:[./\-]\d{2,4})?)\s+([A-Z]{2,4})\b/i;
440
+ // ─── Labeled description format (06/2026) ───────────────────────────────────
441
+ /**
442
+ * Version tag stored in `extendedProperties.private.descriptionFormat` on
443
+ * events created with the labeled format. Lets analytics / future migration
444
+ * scripts distinguish labeled from positional events.
445
+ */
446
+ exports.LABELED_DESCRIPTION_FORMAT_VERSION = "labeled-v1";
447
+ /**
448
+ * Per-field spec for the labeled description format.
449
+ *
450
+ * Each field has:
451
+ * - `label`: canonical label written when building (e.g., "Tutor").
452
+ * - `regex`: extraction regex with capturing groups. Group 1 = value.
453
+ * For `tutor`, group 2 captures the optional QVET id digits.
454
+ *
455
+ * Order-independent: labels anchor each field, so the format tolerates
456
+ * reordering or manual edits as long as the labels are present.
457
+ *
458
+ * Tolerates: `Tutor:`, `tutor :`, `TUTOR: `, trailing whitespace.
459
+ */
460
+ exports.LABELED_DESCRIPTION_FIELDS = {
461
+ tutor: {
462
+ label: "Tutor",
463
+ regex: /^\s*tutor\s*:\s*(.+?)(?:\s*\(\s*Q(\d{4,7})\s*\))?\s*$/im,
464
+ },
465
+ phones: {
466
+ label: "Teléfonos",
467
+ regex: /^\s*tel[eé]fonos?\s*:\s*(.+?)\s*$/im,
468
+ },
469
+ pet: {
470
+ label: "Mascota",
471
+ regex: /^\s*mascota\s*:\s*(.+?)\s*$/im,
472
+ },
473
+ breed: {
474
+ label: "Raza",
475
+ regex: /^\s*raza\s*:\s*(.+?)\s*$/im,
476
+ },
477
+ age: {
478
+ label: "Edad",
479
+ regex: /^\s*edad\s*:\s*(.+?)\s*$/im,
480
+ },
481
+ motivo: {
482
+ label: "Motivo",
483
+ // Motivo is multi-line; extraction handled by extractMotivoLabeled().
484
+ // Regex below only detects presence of the label.
485
+ regex: /^\s*motivo\s*:/im,
486
+ },
487
+ discount: {
488
+ label: "Descuento",
489
+ regex: /^\s*descuento\s*:\s*(.+?)\s*$/im,
490
+ },
491
+ };
492
+ /**
493
+ * Matches a line that starts a new labeled field or the AGENDÓ/REAGENDÓ block
494
+ * or the HVP marker. Used to bound multi-line `Motivo:` capture.
495
+ */
496
+ const LABELED_BOUNDARY_REGEX = /^\s*(?:tutor|tel[eé]fonos?|mascota|raza|edad|descuento|motivo|agend|reagend|—)/i;
497
+ /**
498
+ * Returns "labeled" if any known field label is present at the start of a line
499
+ * in the description; otherwise "positional" (legacy format).
500
+ */
501
+ function detectDescriptionFormat(description) {
502
+ if (!description)
503
+ return "positional";
504
+ for (const field of Object.values(exports.LABELED_DESCRIPTION_FIELDS)) {
505
+ if (field.regex.test(description))
506
+ return "labeled";
507
+ }
508
+ return "positional";
509
+ }
510
+ /**
511
+ * Extract a single-line labeled field. Returns trimmed value or null.
512
+ *
513
+ * For `motivo` (which may span multiple lines), use {@link extractMotivoLabeled}.
514
+ * For the tutor's QVET id, use {@link extractTutorQvetIdLabeled}.
515
+ */
516
+ function extractLabeledField(description, field) {
517
+ if (!description)
518
+ return null;
519
+ const match = description.match(exports.LABELED_DESCRIPTION_FIELDS[field].regex);
520
+ const value = match?.[1]?.trim();
521
+ return value && value.length > 0 ? value : null;
522
+ }
523
+ /**
524
+ * Extract the QVET client id from the `Tutor: NAME (Q12345)` line. Returns the
525
+ * numeric portion as a string (e.g., "12345") or null.
526
+ */
527
+ function extractTutorQvetIdLabeled(description) {
528
+ if (!description)
529
+ return null;
530
+ const match = description.match(exports.LABELED_DESCRIPTION_FIELDS.tutor.regex);
531
+ const id = match?.[2]?.trim();
532
+ return id && id.length > 0 ? id : null;
533
+ }
534
+ /**
535
+ * Extract the motivo block which may span multiple lines. Starts at the
536
+ * `Motivo:` line and collects subsequent lines until another labeled field,
537
+ * an AGENDÓ/REAGENDÓ line, the HVP marker, or end of description.
538
+ */
539
+ function extractMotivoLabeled(description) {
540
+ if (!description)
541
+ return null;
542
+ const lines = description.split("\n");
543
+ const startIdx = lines.findIndex((l) => /^\s*motivo\s*:/i.test(l));
544
+ if (startIdx === -1)
545
+ return null;
546
+ const firstLine = lines[startIdx].replace(/^\s*motivo\s*:\s*/i, "");
547
+ const collected = [firstLine];
548
+ for (let i = startIdx + 1; i < lines.length; i++) {
549
+ if (LABELED_BOUNDARY_REGEX.test(lines[i]))
550
+ break;
551
+ collected.push(lines[i]);
552
+ }
553
+ const value = collected.join("\n").trim();
554
+ return value.length > 0 ? value : null;
555
+ }
436
556
  // ─── Pet age detection ───────────────────────────────────────────────────────
437
557
  /**
438
558
  * Captures pet age expressions in Spanish from descriptions.
@@ -541,4 +661,11 @@ exports.HVP_EVENT_PROP_KEYS = {
541
661
  isPreferredVet: "isPreferredVet",
542
662
  source: "source", // "hvp"
543
663
  standardVersion: "standardVersion",
664
+ /**
665
+ * Marks the description-builder format version used at create/update time
666
+ * (e.g., "labeled-v1"). Informational only — the parser detects format from
667
+ * content, not from this prop. Useful for analytics ("% of events on labeled
668
+ * format") and future migration scripts.
669
+ */
670
+ descriptionFormat: "descriptionFormat",
544
671
  };
@@ -207,6 +207,157 @@ describe("PET_AGE_REGEX", () => {
207
207
  expect("Cytopoint 30MG".match(google_calendar_constants_1.PET_AGE_REGEX)).toBeNull();
208
208
  });
209
209
  });
210
+ describe("Labeled description format (06/2026)", () => {
211
+ const labeledSample = [
212
+ "Tutor: OMAR GOMEZ (Q123456)",
213
+ "Teléfonos: 9992920692 / 9991234567",
214
+ "Mascota: MILO",
215
+ "Raza: Shih Tzu",
216
+ "Edad: 1 año",
217
+ "Motivo: Consulta de seguimiento por dermatitis",
218
+ "Descuento: 15% recordatorio",
219
+ "AGENDÓ JG 14/06/2026",
220
+ "— creada desde HVP app · [HVP]",
221
+ ].join("\n");
222
+ const positionalSample = [
223
+ "OMAR GOMEZ | MILO | Q123456",
224
+ "9992920692",
225
+ "Consulta de seguimiento por dermatitis",
226
+ "Shih Tzu, 1 año",
227
+ "15% recordatorio",
228
+ "AGENDÓ JG 14/06/2026",
229
+ "— creada desde HVP app · [HVP]",
230
+ ].join("\n");
231
+ describe("LABELED_DESCRIPTION_FORMAT_VERSION", () => {
232
+ it("is a stable string", () => {
233
+ expect(google_calendar_constants_1.LABELED_DESCRIPTION_FORMAT_VERSION).toBe("labeled-v1");
234
+ });
235
+ });
236
+ describe("LABELED_DESCRIPTION_FIELDS", () => {
237
+ it("has all expected fields with labels", () => {
238
+ const labels = Object.fromEntries(Object.entries(google_calendar_constants_1.LABELED_DESCRIPTION_FIELDS).map(([k, v]) => [k, v.label]));
239
+ expect(labels).toEqual({
240
+ tutor: "Tutor",
241
+ phones: "Teléfonos",
242
+ pet: "Mascota",
243
+ breed: "Raza",
244
+ age: "Edad",
245
+ motivo: "Motivo",
246
+ discount: "Descuento",
247
+ });
248
+ });
249
+ });
250
+ describe("detectDescriptionFormat", () => {
251
+ it("identifies a labeled description", () => {
252
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)(labeledSample)).toBe("labeled");
253
+ });
254
+ it("identifies a positional (legacy) description", () => {
255
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)(positionalSample)).toBe("positional");
256
+ });
257
+ it("treats empty / undefined as positional", () => {
258
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("")).toBe("positional");
259
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)(undefined)).toBe("positional");
260
+ });
261
+ it("detects when only one label is present", () => {
262
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("Tutor: NORA\n9999999999")).toBe("labeled");
263
+ });
264
+ it("is case-insensitive on the label", () => {
265
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("tutor: NORA\n9999999999")).toBe("labeled");
266
+ expect((0, google_calendar_constants_1.detectDescriptionFormat)("TUTOR: NORA\n9999999999")).toBe("labeled");
267
+ });
268
+ });
269
+ describe("extractLabeledField", () => {
270
+ it("extracts the tutor name (without QVET id)", () => {
271
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "tutor")).toBe("OMAR GOMEZ");
272
+ });
273
+ it("extracts the tutor name when no QVET id is present", () => {
274
+ const desc = "Tutor: NORA DIRCIO\nMascota: KIRI";
275
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "tutor")).toBe("NORA DIRCIO");
276
+ });
277
+ it("extracts phones, pet, breed, age, discount", () => {
278
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "phones")).toBe("9992920692 / 9991234567");
279
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "pet")).toBe("MILO");
280
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "breed")).toBe("Shih Tzu");
281
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "age")).toBe("1 año");
282
+ expect((0, google_calendar_constants_1.extractLabeledField)(labeledSample, "discount")).toBe("15% recordatorio");
283
+ });
284
+ it("returns null when the label is missing", () => {
285
+ const desc = "Tutor: OMAR\nMascota: MILO";
286
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "discount")).toBeNull();
287
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "breed")).toBeNull();
288
+ });
289
+ it("tolerates trailing whitespace and extra spaces around the colon", () => {
290
+ const desc = "Tutor : OMAR GOMEZ \nMascota:MILO";
291
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "tutor")).toBe("OMAR GOMEZ");
292
+ expect((0, google_calendar_constants_1.extractLabeledField)(desc, "pet")).toBe("MILO");
293
+ });
294
+ it("is order-independent", () => {
295
+ const reordered = [
296
+ "Mascota: MILO",
297
+ "Tutor: OMAR GOMEZ (Q123456)",
298
+ "Edad: 1 año",
299
+ ].join("\n");
300
+ expect((0, google_calendar_constants_1.extractLabeledField)(reordered, "tutor")).toBe("OMAR GOMEZ");
301
+ expect((0, google_calendar_constants_1.extractLabeledField)(reordered, "pet")).toBe("MILO");
302
+ expect((0, google_calendar_constants_1.extractLabeledField)(reordered, "age")).toBe("1 año");
303
+ });
304
+ it("returns null for empty description", () => {
305
+ expect((0, google_calendar_constants_1.extractLabeledField)("", "tutor")).toBeNull();
306
+ });
307
+ });
308
+ describe("extractTutorQvetIdLabeled", () => {
309
+ it("extracts the QVET id from the tutor line", () => {
310
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)(labeledSample)).toBe("123456");
311
+ });
312
+ it("returns null when no QVET id is present", () => {
313
+ const desc = "Tutor: NORA DIRCIO\nMascota: KIRI";
314
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)(desc)).toBeNull();
315
+ });
316
+ it("tolerates extra whitespace inside the parens", () => {
317
+ const desc = "Tutor: NORA DIRCIO ( Q987654 )";
318
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)(desc)).toBe("987654");
319
+ });
320
+ it("returns null when the tutor line is missing entirely", () => {
321
+ expect((0, google_calendar_constants_1.extractTutorQvetIdLabeled)("Mascota: KIRI")).toBeNull();
322
+ });
323
+ });
324
+ describe("extractMotivoLabeled", () => {
325
+ it("extracts a single-line motivo", () => {
326
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(labeledSample)).toBe("Consulta de seguimiento por dermatitis");
327
+ });
328
+ it("extracts a multi-line motivo", () => {
329
+ const desc = [
330
+ "Tutor: OMAR",
331
+ "Mascota: MILO",
332
+ "Motivo: Consulta inicial",
333
+ "Antecedentes de alergia",
334
+ "Sospecha de sarna",
335
+ "AGENDÓ JG 14/06/2026",
336
+ ].join("\n");
337
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(desc)).toBe("Consulta inicial\nAntecedentes de alergia\nSospecha de sarna");
338
+ });
339
+ it("stops at the next labeled field even when AGENDÓ is far below", () => {
340
+ const desc = [
341
+ "Tutor: OMAR",
342
+ "Motivo: Solo este motivo",
343
+ "Mascota: MILO",
344
+ "Raza: Shih Tzu",
345
+ "AGENDÓ JG 14/06/2026",
346
+ ].join("\n");
347
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(desc)).toBe("Solo este motivo");
348
+ });
349
+ it("stops at the HVP marker", () => {
350
+ const desc = [
351
+ "Motivo: Consulta",
352
+ "— creada desde HVP app · [HVP]",
353
+ ].join("\n");
354
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)(desc)).toBe("Consulta");
355
+ });
356
+ it("returns null when no motivo label is present", () => {
357
+ expect((0, google_calendar_constants_1.extractMotivoLabeled)("Tutor: OMAR\nMascota: MILO")).toBeNull();
358
+ });
359
+ });
360
+ });
210
361
  describe("KNOWN_BREEDS", () => {
211
362
  it("includes common breeds observed in the discovery data", () => {
212
363
  const lower = google_calendar_constants_1.KNOWN_BREEDS.map((b) => b.toLowerCase());
@@ -24,6 +24,7 @@ export * from './force-majeure.enums';
24
24
  export * from './hris.constants';
25
25
  export * from './payroll-features.constants';
26
26
  export * from './inventory-session.enums';
27
+ export * from './shift-checklist.enums';
27
28
  export * from './documentation.enums';
28
29
  export * from './document.enums';
29
30
  export * from './settlement.enums';
@@ -40,6 +40,7 @@ __exportStar(require("./force-majeure.enums"), exports);
40
40
  __exportStar(require("./hris.constants"), exports);
41
41
  __exportStar(require("./payroll-features.constants"), exports);
42
42
  __exportStar(require("./inventory-session.enums"), exports);
43
+ __exportStar(require("./shift-checklist.enums"), exports);
43
44
  __exportStar(require("./documentation.enums"), exports);
44
45
  __exportStar(require("./document.enums"), exports);
45
46
  __exportStar(require("./settlement.enums"), exports);
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Shift Checklist Enums
3
+ *
4
+ * Enums for the shift checklist module: a per-occasion ("run") list of recurring
5
+ * tasks a collaborator reviews at check-in and before checkout. A master
6
+ * `ChecklistActivity` defines the recurring task; a `ChecklistRun` is one
7
+ * occasion (collaborator + moment + day); a `ChecklistCompletion` records a task
8
+ * done/not-done within a run.
9
+ */
10
+ /**
11
+ * When an activity applies (shown as a tag in the day checklist).
12
+ *
13
+ * - `check_in` (Entrada): start-of-shift tasks
14
+ * - `check_out` (Salida): end-of-shift tasks
15
+ * - `both` (Ambos): relevant at both entrada and salida
16
+ * - `any` (Indistinto): do it once, doesn't matter entrada or salida
17
+ *
18
+ * `both` / `any` are valid ONLY on an activity definition. A run/completion
19
+ * occasion is always `check_in` OR `check_out`.
20
+ */
21
+ export declare enum ChecklistMoment {
22
+ check_in = "check_in",
23
+ check_out = "check_out",
24
+ both = "both",
25
+ any = "any"
26
+ }
27
+ export declare const CHECKLIST_MOMENT_LABELS: Record<ChecklistMoment, string>;
28
+ /** Moments valid for a run/completion occasion (never `both` / `any`). */
29
+ export declare const RUN_MOMENTS: Set<ChecklistMoment>;
30
+ /**
31
+ * Status of a task within a run (three conscious actions; "unmarked" = no action).
32
+ *
33
+ * - `done` (Hecho): the task was performed.
34
+ * - `skipped` (No hacía falta): evaluated and deemed unnecessary (e.g. the
35
+ * sanitary dispensers were already full). Carries a reason.
36
+ * - `not_done` (No se pudo): consciously not done because it couldn't be (e.g.
37
+ * missing material, no time). Carries a reason.
38
+ */
39
+ export declare enum ChecklistCompletionStatus {
40
+ done = "done",
41
+ not_done = "not_done",
42
+ skipped = "skipped"
43
+ }
44
+ export declare const CHECKLIST_COMPLETION_STATUS_LABELS: Record<ChecklistCompletionStatus, string>;
45
+ /** Tooltip help text per status (for the UI controls). */
46
+ export declare const CHECKLIST_COMPLETION_STATUS_HELP: Record<ChecklistCompletionStatus, string>;
47
+ /** Statuses that reset the overdue clock (i.e. count as "the task was done"). */
48
+ export declare const SATISFYING_COMPLETION_STATUSES: Set<ChecklistCompletionStatus>;
49
+ /**
50
+ * How an activity's doneness is scoped across branches.
51
+ *
52
+ * - `specific` (Sucursales específicas): applies to the listed `branchIds`;
53
+ * done/overdue tracked PER branch.
54
+ * - `each` (Todas — cada una): applies to every branch; each branch does
55
+ * its own; done/overdue tracked PER branch (e.g. "Barrer recepción").
56
+ * - `any` (Cualquier sucursal): done ONCE for the whole org regardless of
57
+ * branch; done/overdue tracked GLOBALLY (e.g. "Enviar recordatorios de vacuna").
58
+ */
59
+ export declare enum ChecklistBranchMode {
60
+ specific = "specific",
61
+ each = "each",
62
+ any = "any"
63
+ }
64
+ export declare const CHECKLIST_BRANCH_MODE_LABELS: Record<ChecklistBranchMode, string>;
65
+ /** Branch modes whose doneness is tracked per-branch (not global). */
66
+ export declare const PER_BRANCH_MODES: Set<ChecklistBranchMode>;
67
+ export declare enum ChecklistPriority {
68
+ high = "high",
69
+ medium = "medium",
70
+ low = "low"
71
+ }
72
+ export declare const CHECKLIST_PRIORITY_LABELS: Record<ChecklistPriority, string>;
73
+ /**
74
+ * Lifecycle of a per-occasion checklist run.
75
+ *
76
+ * - `open`: created, collaborator filling it
77
+ * - `submitted`: collaborator confirmed/submitted the run
78
+ */
79
+ export declare enum ChecklistRunStatus {
80
+ open = "open",
81
+ submitted = "submitted"
82
+ }
83
+ export declare const CHECKLIST_RUN_STATUS_LABELS: Record<ChecklistRunStatus, string>;
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ /**
3
+ * Shift Checklist Enums
4
+ *
5
+ * Enums for the shift checklist module: a per-occasion ("run") list of recurring
6
+ * tasks a collaborator reviews at check-in and before checkout. A master
7
+ * `ChecklistActivity` defines the recurring task; a `ChecklistRun` is one
8
+ * occasion (collaborator + moment + day); a `ChecklistCompletion` records a task
9
+ * done/not-done within a run.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CHECKLIST_RUN_STATUS_LABELS = exports.ChecklistRunStatus = exports.CHECKLIST_PRIORITY_LABELS = exports.ChecklistPriority = exports.PER_BRANCH_MODES = exports.CHECKLIST_BRANCH_MODE_LABELS = exports.ChecklistBranchMode = exports.SATISFYING_COMPLETION_STATUSES = exports.CHECKLIST_COMPLETION_STATUS_HELP = exports.CHECKLIST_COMPLETION_STATUS_LABELS = exports.ChecklistCompletionStatus = exports.RUN_MOMENTS = exports.CHECKLIST_MOMENT_LABELS = exports.ChecklistMoment = void 0;
13
+ // --- Moment of shift ---
14
+ /**
15
+ * When an activity applies (shown as a tag in the day checklist).
16
+ *
17
+ * - `check_in` (Entrada): start-of-shift tasks
18
+ * - `check_out` (Salida): end-of-shift tasks
19
+ * - `both` (Ambos): relevant at both entrada and salida
20
+ * - `any` (Indistinto): do it once, doesn't matter entrada or salida
21
+ *
22
+ * `both` / `any` are valid ONLY on an activity definition. A run/completion
23
+ * occasion is always `check_in` OR `check_out`.
24
+ */
25
+ var ChecklistMoment;
26
+ (function (ChecklistMoment) {
27
+ ChecklistMoment["check_in"] = "check_in";
28
+ ChecklistMoment["check_out"] = "check_out";
29
+ ChecklistMoment["both"] = "both";
30
+ ChecklistMoment["any"] = "any";
31
+ })(ChecklistMoment || (exports.ChecklistMoment = ChecklistMoment = {}));
32
+ exports.CHECKLIST_MOMENT_LABELS = {
33
+ [ChecklistMoment.check_in]: "Entrada",
34
+ [ChecklistMoment.check_out]: "Salida",
35
+ [ChecklistMoment.both]: "Ambos",
36
+ [ChecklistMoment.any]: "Indistinto",
37
+ };
38
+ /** Moments valid for a run/completion occasion (never `both` / `any`). */
39
+ exports.RUN_MOMENTS = new Set([
40
+ ChecklistMoment.check_in,
41
+ ChecklistMoment.check_out,
42
+ ]);
43
+ // --- Completion status ---
44
+ /**
45
+ * Status of a task within a run (three conscious actions; "unmarked" = no action).
46
+ *
47
+ * - `done` (Hecho): the task was performed.
48
+ * - `skipped` (No hacía falta): evaluated and deemed unnecessary (e.g. the
49
+ * sanitary dispensers were already full). Carries a reason.
50
+ * - `not_done` (No se pudo): consciously not done because it couldn't be (e.g.
51
+ * missing material, no time). Carries a reason.
52
+ */
53
+ var ChecklistCompletionStatus;
54
+ (function (ChecklistCompletionStatus) {
55
+ ChecklistCompletionStatus["done"] = "done";
56
+ ChecklistCompletionStatus["not_done"] = "not_done";
57
+ ChecklistCompletionStatus["skipped"] = "skipped";
58
+ })(ChecklistCompletionStatus || (exports.ChecklistCompletionStatus = ChecklistCompletionStatus = {}));
59
+ exports.CHECKLIST_COMPLETION_STATUS_LABELS = {
60
+ [ChecklistCompletionStatus.done]: "Hecho",
61
+ [ChecklistCompletionStatus.skipped]: "No hacía falta",
62
+ [ChecklistCompletionStatus.not_done]: "No se pudo",
63
+ };
64
+ /** Tooltip help text per status (for the UI controls). */
65
+ exports.CHECKLIST_COMPLETION_STATUS_HELP = {
66
+ [ChecklistCompletionStatus.done]: "Se realizó.",
67
+ [ChecklistCompletionStatus.skipped]: "Se evaluó y no era necesario hacerla (ej. las sanitas ya estaban llenas).",
68
+ [ChecklistCompletionStatus.not_done]: "Conscientemente no se hizo porque no se pudo (ej. faltó material, no dio tiempo).",
69
+ };
70
+ /** Statuses that reset the overdue clock (i.e. count as "the task was done"). */
71
+ exports.SATISFYING_COMPLETION_STATUSES = new Set([
72
+ ChecklistCompletionStatus.done,
73
+ ]);
74
+ // --- Branch mode (how an activity relates to branches) ---
75
+ /**
76
+ * How an activity's doneness is scoped across branches.
77
+ *
78
+ * - `specific` (Sucursales específicas): applies to the listed `branchIds`;
79
+ * done/overdue tracked PER branch.
80
+ * - `each` (Todas — cada una): applies to every branch; each branch does
81
+ * its own; done/overdue tracked PER branch (e.g. "Barrer recepción").
82
+ * - `any` (Cualquier sucursal): done ONCE for the whole org regardless of
83
+ * branch; done/overdue tracked GLOBALLY (e.g. "Enviar recordatorios de vacuna").
84
+ */
85
+ var ChecklistBranchMode;
86
+ (function (ChecklistBranchMode) {
87
+ ChecklistBranchMode["specific"] = "specific";
88
+ ChecklistBranchMode["each"] = "each";
89
+ ChecklistBranchMode["any"] = "any";
90
+ })(ChecklistBranchMode || (exports.ChecklistBranchMode = ChecklistBranchMode = {}));
91
+ exports.CHECKLIST_BRANCH_MODE_LABELS = {
92
+ [ChecklistBranchMode.specific]: "Sucursales específicas",
93
+ [ChecklistBranchMode.each]: "Todas (cada una)",
94
+ [ChecklistBranchMode.any]: "Cualquier sucursal",
95
+ };
96
+ /** Branch modes whose doneness is tracked per-branch (not global). */
97
+ exports.PER_BRANCH_MODES = new Set([
98
+ ChecklistBranchMode.specific,
99
+ ChecklistBranchMode.each,
100
+ ]);
101
+ // --- Priority ---
102
+ var ChecklistPriority;
103
+ (function (ChecklistPriority) {
104
+ ChecklistPriority["high"] = "high";
105
+ ChecklistPriority["medium"] = "medium";
106
+ ChecklistPriority["low"] = "low";
107
+ })(ChecklistPriority || (exports.ChecklistPriority = ChecklistPriority = {}));
108
+ exports.CHECKLIST_PRIORITY_LABELS = {
109
+ [ChecklistPriority.high]: "Alta",
110
+ [ChecklistPriority.medium]: "Media",
111
+ [ChecklistPriority.low]: "Baja",
112
+ };
113
+ // --- Run status ---
114
+ /**
115
+ * Lifecycle of a per-occasion checklist run.
116
+ *
117
+ * - `open`: created, collaborator filling it
118
+ * - `submitted`: collaborator confirmed/submitted the run
119
+ */
120
+ var ChecklistRunStatus;
121
+ (function (ChecklistRunStatus) {
122
+ ChecklistRunStatus["open"] = "open";
123
+ ChecklistRunStatus["submitted"] = "submitted";
124
+ })(ChecklistRunStatus || (exports.ChecklistRunStatus = ChecklistRunStatus = {}));
125
+ exports.CHECKLIST_RUN_STATUS_LABELS = {
126
+ [ChecklistRunStatus.open]: "Abierto",
127
+ [ChecklistRunStatus.submitted]: "Enviado",
128
+ };
@@ -85,4 +85,16 @@ export interface CreateAppointmentRequest {
85
85
  * collaborators list. If omitted, the auth user's col_code is used.
86
86
  */
87
87
  schedulerColCode?: string;
88
+ /**
89
+ * Optional manual override for the description body. When present, the
90
+ * backend uses this verbatim instead of building the description from the
91
+ * structured fields above. The backend still appends the AGENDÓ/REAGENDÓ
92
+ * scheduler line and the HVP marker so attribution and provenance are not
93
+ * lost.
94
+ *
95
+ * Used when the receptionist enables "Editar texto" in the form to fine-tune
96
+ * wording before saving. On PATCH, supplying this also short-circuits the
97
+ * labeled rebuild — useful for surgical edits.
98
+ */
99
+ descriptionOverride?: string;
88
100
  }
@@ -13,6 +13,7 @@ export * from './time-off-request';
13
13
  export * from './force-majeure';
14
14
  export * from './job';
15
15
  export * from './inventory-session';
16
+ export * from './shift-checklist';
16
17
  export * from './client-billing';
17
18
  export * from './global-invoice';
18
19
  export * from './inventory-report';
@@ -29,6 +29,7 @@ __exportStar(require("./time-off-request"), exports);
29
29
  __exportStar(require("./force-majeure"), exports);
30
30
  __exportStar(require("./job"), exports);
31
31
  __exportStar(require("./inventory-session"), exports);
32
+ __exportStar(require("./shift-checklist"), exports);
32
33
  __exportStar(require("./client-billing"), exports);
33
34
  __exportStar(require("./global-invoice"), exports);
34
35
  __exportStar(require("./inventory-report"), exports);
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './requests';
3
+ export * from './responses';
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./requests"), exports);
19
+ __exportStar(require("./responses"), exports);
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Shift Checklist Request Types
3
+ *
4
+ * Request DTOs for the shift checklist endpoints.
5
+ *
6
+ * @example POST /api/shift-checklist/activities
7
+ * @example GET /api/shift-checklist/runs/view
8
+ * @example POST /api/shift-checklist/runs/:id/completions/batch
9
+ */
10
+ import { ChecklistBranchMode, ChecklistCompletionStatus, ChecklistMoment, ChecklistPriority } from '../../constants/shift-checklist.enums';
11
+ /**
12
+ * Create a master checklist activity.
13
+ *
14
+ * @example POST /api/shift-checklist/activities
15
+ */
16
+ export interface CreateChecklistActivityRequest {
17
+ /** Concrete, verb-first action (location lives in the name when it matters) */
18
+ name: string;
19
+ /** Branch scope: specific (these branchIds) / each (all, per-branch) / any (global) */
20
+ branchMode: ChecklistBranchMode;
21
+ /** Branch ids the activity applies to (required when branchMode is `specific`) */
22
+ branchIds?: string[];
23
+ momentOfShift: ChecklistMoment;
24
+ /** When false, the activity is a visible reminder only (default true). */
25
+ isTracked?: boolean;
26
+ /** Days between occurrences; null = no fixed cadence (never overdue) */
27
+ frequencyDays?: number | null;
28
+ priority?: ChecklistPriority | null;
29
+ estimatedMinutes?: number | null;
30
+ /** How/when to do it: merged procedure + conditional notes (free text, may be multiline). */
31
+ instructions?: string;
32
+ requiresEvidence?: boolean;
33
+ }
34
+ /**
35
+ * Update a master checklist activity (partial).
36
+ *
37
+ * @example PATCH /api/shift-checklist/activities/:id
38
+ */
39
+ export interface UpdateChecklistActivityRequest {
40
+ name?: string;
41
+ branchMode?: ChecklistBranchMode;
42
+ branchIds?: string[];
43
+ momentOfShift?: ChecklistMoment;
44
+ isTracked?: boolean;
45
+ frequencyDays?: number | null;
46
+ priority?: ChecklistPriority | null;
47
+ estimatedMinutes?: number | null;
48
+ /** How/when to do it: merged procedure + conditional notes (free text, may be multiline). */
49
+ instructions?: string;
50
+ requiresEvidence?: boolean;
51
+ isActive?: boolean;
52
+ }
53
+ /**
54
+ * Filters for listing master activities.
55
+ *
56
+ * @example GET /api/shift-checklist/activities?branchId=...&momentOfShift=check_out&isActive=true
57
+ */
58
+ export interface ListChecklistActivitiesFilters {
59
+ branchId?: string;
60
+ momentOfShift?: ChecklistMoment;
61
+ isActive?: boolean;
62
+ }
63
+ /**
64
+ * Query for the checkout view (find-or-create a run + its assembled view).
65
+ *
66
+ * @example GET /api/shift-checklist/runs/view?momentOfShift=check_out&branchId=...
67
+ */
68
+ export interface GetChecklistViewQuery {
69
+ /** check_in or check_out (never both) */
70
+ momentOfShift: ChecklistMoment;
71
+ branchId: string;
72
+ /** YYYY-MM-DD; defaults to today (America/Mexico_City) server-side */
73
+ shiftDate?: string;
74
+ /** link to the realized attendance occasion (TimeShift) when opened from checkout */
75
+ attendanceRecordId?: string;
76
+ }
77
+ /**
78
+ * Update a run (e.g. correct the branch for the whole run, or add notes).
79
+ *
80
+ * @example PATCH /api/shift-checklist/runs/:id
81
+ */
82
+ export interface UpdateChecklistRunRequest {
83
+ branchId?: string;
84
+ notes?: string;
85
+ }
86
+ /**
87
+ * A single completion to record/update within a run.
88
+ * Either `activityId` (master task) or `adHocTitle` (ad-hoc task) must be set.
89
+ */
90
+ export interface RecordCompletionItemRequest {
91
+ /** null/omitted = ad-hoc task (requires adHocTitle) */
92
+ activityId?: string | null;
93
+ adHocTitle?: string;
94
+ status: ChecklistCompletionStatus;
95
+ /** who did it; defaults to [current user]. Multiple for shared tasks. */
96
+ performedByIds?: string[];
97
+ /** required when status is "skipped" */
98
+ skipReason?: string;
99
+ notes?: string;
100
+ evidenceUrl?: string | null;
101
+ }
102
+ /**
103
+ * Batch upsert completions in a run (idempotent per activity).
104
+ *
105
+ * @example POST /api/shift-checklist/runs/:id/completions/batch
106
+ */
107
+ export interface RecordCompletionsBatchRequest {
108
+ items: RecordCompletionItemRequest[];
109
+ }
110
+ /**
111
+ * Query filters for checklist statistics.
112
+ *
113
+ * @example GET /api/shift-checklist/stats?branchId=...&from=2026-06-01&to=2026-06-20
114
+ */
115
+ export interface ChecklistStatsQuery {
116
+ branchId?: string;
117
+ from?: string;
118
+ to?: string;
119
+ }
120
+ /**
121
+ * Create a one-off pendiente (just a title; stays open until resolved).
122
+ *
123
+ * @example POST /api/shift-checklist/pending-tasks
124
+ */
125
+ export interface CreateChecklistPendingTaskRequest {
126
+ title: string;
127
+ branchId: string;
128
+ }
129
+ /**
130
+ * Mark a pendiente done, recording who did it.
131
+ *
132
+ * @example PATCH /api/shift-checklist/pending-tasks/:id/resolve
133
+ */
134
+ export interface ResolveChecklistPendingTaskRequest {
135
+ performedByIds: string[];
136
+ }
137
+ /**
138
+ * Filters for listing pendientes.
139
+ *
140
+ * @example GET /api/shift-checklist/pending-tasks?branchId=...&includeResolved=false
141
+ */
142
+ export interface ListChecklistPendingTasksFilters {
143
+ branchId: string;
144
+ includeResolved?: boolean;
145
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * Shift Checklist Request Types
4
+ *
5
+ * Request DTOs for the shift checklist endpoints.
6
+ *
7
+ * @example POST /api/shift-checklist/activities
8
+ * @example GET /api/shift-checklist/runs/view
9
+ * @example POST /api/shift-checklist/runs/:id/completions/batch
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Shift Checklist Response Types
3
+ *
4
+ * Response DTOs for the shift checklist endpoints. Dates are ISO 8601 strings.
5
+ */
6
+ import { ChecklistBranchMode, ChecklistMoment, ChecklistPriority, ChecklistRunStatus } from '../../constants/shift-checklist.enums';
7
+ import { ChecklistAdHocItem, ChecklistCheckoutItem } from './types';
8
+ /**
9
+ * A master checklist activity.
10
+ *
11
+ * @example GET /api/shift-checklist/activities
12
+ */
13
+ export interface ChecklistActivityResponse {
14
+ id: string;
15
+ name: string;
16
+ branchMode: ChecklistBranchMode;
17
+ branchIds: string[];
18
+ momentOfShift: ChecklistMoment;
19
+ isTracked: boolean;
20
+ frequencyDays: number | null;
21
+ priority: ChecklistPriority | null;
22
+ estimatedMinutes: number | null;
23
+ /** How/when to do it: merged procedure + conditional notes (free text, may be multiline). */
24
+ instructions: string;
25
+ requiresEvidence: boolean;
26
+ isActive: boolean;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ createdBy: string;
30
+ updatedBy: string;
31
+ }
32
+ /**
33
+ * A per-occasion checklist run.
34
+ *
35
+ * @example GET /api/shift-checklist/runs/view
36
+ */
37
+ export interface ChecklistRunResponse {
38
+ id: string;
39
+ collaboratorId: string;
40
+ branchId: string;
41
+ branchName: string;
42
+ momentOfShift: ChecklistMoment;
43
+ shiftDate: string;
44
+ attendanceRecordId: string | null;
45
+ status: ChecklistRunStatus;
46
+ submittedAt: string | null;
47
+ notes: string;
48
+ }
49
+ /**
50
+ * The assembled checkout view: the run, its activity rows (with last-done +
51
+ * overdue), and any ad-hoc items. Powers the checkout modal.
52
+ *
53
+ * @example GET /api/shift-checklist/runs/view
54
+ * @example POST /api/shift-checklist/runs/:id/completions/batch (refreshed view)
55
+ */
56
+ export interface ChecklistCheckoutViewResponse {
57
+ run: ChecklistRunResponse;
58
+ items: ChecklistCheckoutItem[];
59
+ adHocItems: ChecklistAdHocItem[];
60
+ }
61
+ export interface ChecklistBranchStats {
62
+ branchId: string;
63
+ branchName: string;
64
+ totalCompletions: number;
65
+ doneCount: number;
66
+ notDoneCount: number;
67
+ skippedCount: number;
68
+ /** master activities currently overdue at this branch */
69
+ overdueActivityCount: number;
70
+ }
71
+ /**
72
+ * Checklist statistics over a period.
73
+ *
74
+ * @example GET /api/shift-checklist/stats
75
+ */
76
+ export interface ChecklistStatsResponse {
77
+ from: string | null;
78
+ to: string | null;
79
+ byBranch: ChecklistBranchStats[];
80
+ totalRuns: number;
81
+ /** done completions / total completions, 0..1 */
82
+ completionRate: number;
83
+ }
84
+ /**
85
+ * A one-off "pendiente" — created on the fly, stays open per branch across shifts
86
+ * until someone marks it done (and only then records who did it).
87
+ *
88
+ * @example GET /api/shift-checklist/pending-tasks?branchId=...
89
+ */
90
+ export interface ChecklistPendingTaskResponse {
91
+ id: string;
92
+ title: string;
93
+ branchId: string;
94
+ isResolved: boolean;
95
+ doneByIds: string[];
96
+ doneByNames: string[];
97
+ doneAt: string | null;
98
+ createdBy: string;
99
+ createdAt: string;
100
+ }
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ /**
3
+ * Shift Checklist Response Types
4
+ *
5
+ * Response DTOs for the shift checklist endpoints. Dates are ISO 8601 strings.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Shift Checklist — shared building-block types
3
+ *
4
+ * Reusable shapes referenced by the endpoint responses (checkout view items,
5
+ * completion shape). Dates are ISO 8601 strings on the wire.
6
+ */
7
+ import { ChecklistBranchMode, ChecklistCompletionStatus, ChecklistMoment, ChecklistPriority } from '../../constants/shift-checklist.enums';
8
+ /**
9
+ * A recorded completion of a task within a run (or an ad-hoc task).
10
+ *
11
+ * @example POST /api/shift-checklist/runs/:id/completions/batch
12
+ */
13
+ export interface ChecklistCompletionResponse {
14
+ id: string;
15
+ runId: string;
16
+ /** null = ad-hoc task (not on the master list) */
17
+ activityId: string | null;
18
+ /** present when activityId is null */
19
+ adHocTitle: string | null;
20
+ status: ChecklistCompletionStatus;
21
+ /** present when status is "skipped" */
22
+ skipReason: string | null;
23
+ /** who performed the task (one or more, for shared tasks) */
24
+ performedByIds: string[];
25
+ performedByNames: string[];
26
+ /** who logged the completion */
27
+ reportedById: string;
28
+ evidenceUrl: string | null;
29
+ notes: string;
30
+ completedAt: string | null;
31
+ /** denormalized from the run */
32
+ branchId: string;
33
+ shiftDate: string;
34
+ momentOfShift: ChecklistMoment;
35
+ }
36
+ /**
37
+ * One row in the checkout view: a master activity plus its last-done history and
38
+ * this run's completion (if any).
39
+ */
40
+ export interface ChecklistCheckoutItem {
41
+ activityId: string;
42
+ name: string;
43
+ /** When the activity is meant to be done (shown as a tag in the day list). */
44
+ momentOfShift: ChecklistMoment;
45
+ /** Branch scope — `any` items count globally and are grouped apart. */
46
+ branchMode: ChecklistBranchMode;
47
+ /** When false, the activity is a visible reminder only — not marked/tracked. */
48
+ isTracked: boolean;
49
+ priority: ChecklistPriority | null;
50
+ estimatedMinutes: number | null;
51
+ instructions: string;
52
+ requiresEvidence: boolean;
53
+ frequencyDays: number | null;
54
+ /** last time this activity was DONE at this branch (drives overdue) */
55
+ lastDoneAt: string | null;
56
+ /** who did it last (may be several for a shared task) */
57
+ lastDoneByIds: string[];
58
+ lastDoneByNames: string[];
59
+ /**
60
+ * Most recent action on this activity at this branch, ANY status — for
61
+ * context when the last thing that happened was an omission (e.g. last done
62
+ * Jan 1 but last action was "omitida" yesterday). Null if never acted on.
63
+ */
64
+ lastAction: {
65
+ status: ChecklistCompletionStatus;
66
+ at: string;
67
+ byIds: string[];
68
+ byNames: string[];
69
+ skipReason: string | null;
70
+ notes: string;
71
+ } | null;
72
+ /** lastDoneAt + frequencyDays < now (false when no cadence or never done) */
73
+ isOverdue: boolean;
74
+ /** this run's completion for the activity, if recorded */
75
+ completion: ChecklistCompletionResponse | null;
76
+ }
77
+ /** An ad-hoc task added during a run (not tied to a master activity). */
78
+ export interface ChecklistAdHocItem {
79
+ id: string;
80
+ adHocTitle: string;
81
+ status: ChecklistCompletionStatus;
82
+ performedByIds: string[];
83
+ performedByNames: string[];
84
+ completedAt: string | null;
85
+ notes: string;
86
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ /**
3
+ * Shift Checklist — shared building-block types
4
+ *
5
+ * Reusable shapes referenced by the endpoint responses (checkout view items,
6
+ * completion shape). Dates are ISO 8601 strings on the wire.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -9,3 +9,4 @@ export * from './email.validation';
9
9
  export * from './phone.validation';
10
10
  export * from './address.validation';
11
11
  export * from './document.validation';
12
+ export * from './shift-checklist.validation';
@@ -25,3 +25,4 @@ __exportStar(require("./email.validation"), exports);
25
25
  __exportStar(require("./phone.validation"), exports);
26
26
  __exportStar(require("./address.validation"), exports);
27
27
  __exportStar(require("./document.validation"), exports);
28
+ __exportStar(require("./shift-checklist.validation"), exports);
@@ -0,0 +1,12 @@
1
+ import { ValidationResult } from './rfc.validation';
2
+ /**
3
+ * Validates a checklist activity frequency (in days).
4
+ *
5
+ * The frequency is the number of days between occurrences of a recurring task.
6
+ * - `null`/`undefined` → no fixed cadence (valid; the task is never "overdue").
7
+ * - otherwise → must be an integer ≥ 1.
8
+ *
9
+ * @param value - frequency in days, or null/undefined for "no cadence"
10
+ * @returns ValidationResult with isValid flag and optional error message
11
+ */
12
+ export declare function validateChecklistFrequency(value: number | null | undefined): ValidationResult;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateChecklistFrequency = validateChecklistFrequency;
4
+ /**
5
+ * Validates a checklist activity frequency (in days).
6
+ *
7
+ * The frequency is the number of days between occurrences of a recurring task.
8
+ * - `null`/`undefined` → no fixed cadence (valid; the task is never "overdue").
9
+ * - otherwise → must be an integer ≥ 1.
10
+ *
11
+ * @param value - frequency in days, or null/undefined for "no cadence"
12
+ * @returns ValidationResult with isValid flag and optional error message
13
+ */
14
+ function validateChecklistFrequency(value) {
15
+ if (value === null || value === undefined) {
16
+ return { isValid: true };
17
+ }
18
+ if (typeof value !== 'number' || Number.isNaN(value) || !Number.isInteger(value)) {
19
+ return {
20
+ isValid: false,
21
+ error: 'La frecuencia debe ser un número entero de días',
22
+ };
23
+ }
24
+ if (value < 1) {
25
+ return { isValid: false, error: 'La frecuencia debe ser al menos 1 día' };
26
+ }
27
+ return { isValid: true };
28
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const shift_checklist_validation_1 = require("./shift-checklist.validation");
4
+ describe('validateChecklistFrequency', () => {
5
+ describe('valid frequencies', () => {
6
+ it('accepts null (no fixed cadence)', () => {
7
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(null).isValid).toBe(true);
8
+ });
9
+ it('accepts undefined (no fixed cadence)', () => {
10
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(undefined).isValid).toBe(true);
11
+ });
12
+ it('accepts 1 (daily)', () => {
13
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(1).isValid).toBe(true);
14
+ });
15
+ it('accepts arbitrary positive integers', () => {
16
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(7).isValid).toBe(true);
17
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(30).isValid).toBe(true);
18
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(45).isValid).toBe(true);
19
+ });
20
+ });
21
+ describe('invalid frequencies', () => {
22
+ it('rejects 0', () => {
23
+ const result = (0, shift_checklist_validation_1.validateChecklistFrequency)(0);
24
+ expect(result.isValid).toBe(false);
25
+ expect(result.error).toBeDefined();
26
+ });
27
+ it('rejects negative numbers', () => {
28
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(-1).isValid).toBe(false);
29
+ });
30
+ it('rejects non-integers', () => {
31
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(1.5).isValid).toBe(false);
32
+ });
33
+ it('rejects NaN', () => {
34
+ expect((0, shift_checklist_validation_1.validateChecklistFrequency)(Number.NaN).isValid).toBe(false);
35
+ });
36
+ });
37
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hvp-shared",
3
- "version": "13.26.0",
3
+ "version": "13.35.0",
4
4
  "description": "Shared types and utilities for HVP backend and frontend",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",