@primocaredentgroup/convex-campaigns-component 0.3.1 → 0.3.3

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.
@@ -34,7 +34,8 @@ export const SUPPRESSION_REASONS = [
34
34
  "LOWER_PRIORITY",
35
35
  ] as const;
36
36
 
37
- export const scopeTypeValidator = v.union(v.literal("HQ"), v.literal("CLINIC"));
37
+ // HQ e CLINIC sono i valori predefiniti; l'host può usare altri codici (REGION, AREA, ecc.)
38
+ export const scopeTypeValidator = v.string();
38
39
  export const campaignStatusValidator = v.union(
39
40
  v.literal("DRAFT"),
40
41
  v.literal("RUNNING"),
@@ -106,7 +107,7 @@ export const scheduleSpecValidator = v.object({
106
107
  delayMinutes: v.optional(v.number()),
107
108
  });
108
109
 
109
- export type CampaignScopeType = (typeof CAMPAIGN_SCOPE_TYPES)[number];
110
+ export type CampaignScopeType = string;
110
111
  export type CampaignStatus = (typeof CAMPAIGN_STATUSES)[number];
111
112
  export type StepType = (typeof STEP_TYPES)[number];
112
113
  export type SnapshotStatus = (typeof SNAPSHOT_STATUSES)[number];
@@ -40,7 +40,12 @@ async function queryUserByEmailWithFallback(ctx: any, email: string) {
40
40
  }
41
41
 
42
42
  const users = await ctx.db.query("users").collect();
43
- return users.find((user: any) => user?.isActive && user?.email === email) ?? null;
43
+ return (
44
+ users.find(
45
+ (u: any) =>
46
+ u?.isActive && u?.email?.toLowerCase() === email?.toLowerCase(),
47
+ ) ?? null
48
+ );
44
49
  }
45
50
 
46
51
  export const resolveRolesForIdentity = internalQuery({
@@ -70,10 +75,18 @@ export const resolveRolesForIdentity = internalQuery({
70
75
  const directRole = normalizeRole(user.role);
71
76
  if (directRole) roles.add(directRole);
72
77
 
73
- const userClinicRoles = await ctx.db
74
- .query("user_clinic_roles")
75
- .withIndex("by_user", (q) => q.eq("userId", user._id))
76
- .collect();
78
+ let userClinicRoles: Array<{ isActive?: boolean; roleId: string }>;
79
+ try {
80
+ userClinicRoles = await ctx.db
81
+ .query("user_clinic_roles")
82
+ .withIndex("by_user", (q) => q.eq("userId", user._id))
83
+ .collect();
84
+ } catch (error) {
85
+ if (!isIndexError(error)) throw error;
86
+ userClinicRoles = (
87
+ await ctx.db.query("user_clinic_roles").collect()
88
+ ).filter((r: any) => r.userId === user._id);
89
+ }
77
90
 
78
91
  for (const userClinicRole of userClinicRoles) {
79
92
  if (!userClinicRole.isActive) continue;
@@ -40,7 +40,7 @@ export const createCampaign = mutation({
40
40
  name: v.string(),
41
41
  description: v.optional(v.string()),
42
42
  ownerUserId: v.optional(v.id("users")),
43
- scopeType: v.union(v.literal("HQ"), v.literal("CLINIC")),
43
+ scopeType: v.string(),
44
44
  scopeClinicIds: v.array(clinicScopeIdValidator),
45
45
  priority: v.number(),
46
46
  defaultRunConfig: v.optional(runConfigValidator),
@@ -67,7 +67,7 @@ export const updateCampaign = mutation({
67
67
  name: v.optional(v.string()),
68
68
  description: v.optional(v.string()),
69
69
  ownerUserId: v.optional(v.id("users")),
70
- scopeType: v.optional(v.union(v.literal("HQ"), v.literal("CLINIC"))),
70
+ scopeType: v.optional(v.string()),
71
71
  scopeClinicIds: v.optional(v.array(clinicScopeIdValidator)),
72
72
  priority: v.optional(v.number()),
73
73
  defaultRunConfig: v.optional(runConfigValidator),
@@ -56,7 +56,12 @@ async function queryUserByEmailWithFallback(ctx: AnyCtx, email: string) {
56
56
  }
57
57
 
58
58
  const users = await ctx.db.query("users").collect();
59
- return users.find((user: any) => user?.isActive && user?.email === email) ?? null;
59
+ return (
60
+ users.find(
61
+ (u: any) =>
62
+ u?.isActive && u?.email?.toLowerCase() === email?.toLowerCase(),
63
+ ) ?? null
64
+ );
60
65
  }
61
66
 
62
67
  async function resolveCurrentUserWithDb(ctx: AnyCtx) {
@@ -86,10 +91,19 @@ async function getUserRoleCodes(ctx: AnyCtx, userId: Id<"users">): Promise<Set<s
86
91
  roles.add(normalizeRole(user.role));
87
92
  }
88
93
 
89
- const userClinicRoles = await ctx.db
90
- .query("user_clinic_roles")
91
- .withIndex("by_user", (q: any) => q.eq("userId", userId))
92
- .collect();
94
+ let userClinicRoles: any[] = [];
95
+ try {
96
+ userClinicRoles = await ctx.db
97
+ .query("user_clinic_roles")
98
+ .withIndex("by_user", (q: any) => q.eq("userId", userId))
99
+ .collect();
100
+ } catch (error) {
101
+ if (!isIndexError(error)) throw error;
102
+ userClinicRoles = (await ctx.db.query("user_clinic_roles").collect()).filter(
103
+ (r: any) => r.userId === userId,
104
+ );
105
+ }
106
+
93
107
  for (const userClinicRole of userClinicRoles) {
94
108
  if (!userClinicRole.isActive) continue;
95
109
  const roleDoc = await ctx.db.get(userClinicRole.roleId);
@@ -2,8 +2,8 @@ import type { QueryCtx, MutationCtx } from "../../../_generated/server";
2
2
  import type { Doc, Id } from "../../../_generated/dataModel";
3
3
 
4
4
  export type CampaignScope = {
5
- scopeType: "HQ" | "CLINIC";
6
- scopeClinicIds: string[];
5
+ scopeType: string;
6
+ scopeClinicIds: (Id<"clinics"> | string)[];
7
7
  };
8
8
 
9
9
  export type SegmentationRules = {
@@ -98,7 +98,7 @@ async function queryNoShowCandidates(
98
98
  if (!hasNoShow) continue;
99
99
 
100
100
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
101
- if (campaignScope.scopeType === "CLINIC" && !clinicId) continue;
101
+ if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
102
102
  patients.push({
103
103
  patientId: patient._id,
104
104
  clinicId,
@@ -163,7 +163,7 @@ async function queryRecallOrQuoteFollowupCandidates(
163
163
  const patients: CandidatePatient[] = [];
164
164
  for (const patient of page.page) {
165
165
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
166
- if (campaignScope.scopeType === "CLINIC" && !clinicId) continue;
166
+ if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
167
167
 
168
168
  const appointments = await ctx.db
169
169
  .query("appointments")
@@ -218,7 +218,7 @@ export async function queryCandidates(
218
218
  const patient = await ctx.db.get(patientId);
219
219
  if (!patient || patient.status === "archived") continue;
220
220
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
221
- if (campaignScope.scopeType === "CLINIC" && !clinicId) continue;
221
+ if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
222
222
  patients.push({
223
223
  patientId: patient._id,
224
224
  clinicId,
@@ -260,7 +260,7 @@ export async function queryCandidates(
260
260
  const patients: CandidatePatient[] = [];
261
261
  for (const patient of page.page) {
262
262
  const clinicId = await deriveClinicIdFromAppointments(ctx, patient, campaignScope);
263
- if (campaignScope.scopeType === "CLINIC" && !clinicId) continue;
263
+ if (campaignScope.scopeType !== "HQ" && !clinicId) continue;
264
264
  patients.push({
265
265
  patientId: patient._id,
266
266
  clinicId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primocaredentgroup/convex-campaigns-component",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Convex Campaigns backend component for PrimoCore",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",