@encatch/web-sdk 0.0.35 → 1.0.0-beta.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/README.md +140 -638
- package/dist/encatch.es.js +2 -0
- package/dist/encatch.es.js.map +1 -0
- package/dist/encatch.iife.js +2 -0
- package/dist/encatch.iife.js.map +1 -0
- package/dist/index.d.ts +191 -0
- package/package.json +32 -50
- package/dist-sdk/plugin/sdk/core-wrapper-BMvOyc0u.js +0 -22926
- package/dist-sdk/plugin/sdk/module-DC2Edddk.js +0 -481
- package/dist-sdk/plugin/sdk/module.js +0 -5
- package/dist-sdk/plugin/sdk/preview-sdk.html +0 -1182
- package/dist-sdk/plugin/sdk/vite.svg +0 -15
- package/dist-sdk/plugin/sdk/web-form-engine-core.css +0 -1
- package/index.d.ts +0 -207
- package/src/@types/encatch-type.ts +0 -111
- package/src/encatch-instance.ts +0 -161
- package/src/feedback-api-types.ts +0 -18
- package/src/hooks/useDevice.ts +0 -30
- package/src/hooks/useFeedbackInterval.ts +0 -71
- package/src/hooks/useFeedbackTriggers.ts +0 -342
- package/src/hooks/useFetchElligibleFeedbackConfiguration.ts +0 -363
- package/src/hooks/useFetchFeedbackConfigurationDetails.ts +0 -92
- package/src/hooks/usePageChangeTracker.ts +0 -88
- package/src/hooks/usePrepopulatedAnswers.ts +0 -123
- package/src/hooks/useRefineTextForm.ts +0 -55
- package/src/hooks/useSubmitFeedbackForm.ts +0 -134
- package/src/hooks/useUserSession.ts +0 -53
- package/src/module.tsx +0 -428
- package/src/store/formResponses.ts +0 -211
- package/src/utils/browser-details.ts +0 -36
- package/src/utils/duration-utils.ts +0 -158
- package/src/utils/feedback-frequency-storage.ts +0 -214
- package/src/utils/feedback-storage.ts +0 -166
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
import { signal, computed } from "@preact/signals";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Answer,
|
|
5
|
-
CombinedQuestion,
|
|
6
|
-
QuestionResponse,
|
|
7
|
-
QuestionType,
|
|
8
|
-
QuestionTypes,
|
|
9
|
-
} from "@encatch/schema";
|
|
10
|
-
|
|
11
|
-
// Type for the entire form responses store
|
|
12
|
-
interface FormResponsesStore {
|
|
13
|
-
questions: QuestionResponse[];
|
|
14
|
-
questionsData: Record<string, CombinedQuestion>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Create the initial store state
|
|
18
|
-
const initialStore: FormResponsesStore = {
|
|
19
|
-
questions: [],
|
|
20
|
-
questionsData: {},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Create signals for the store
|
|
24
|
-
const formResponsesSignal = signal<FormResponsesStore>(initialStore);
|
|
25
|
-
|
|
26
|
-
// Create a computed property to access the store (similar to Solid's store access)
|
|
27
|
-
const formResponses = computed(() => formResponsesSignal.value);
|
|
28
|
-
|
|
29
|
-
// Helper function to update the store
|
|
30
|
-
const setFormResponses = (
|
|
31
|
-
key: keyof FormResponsesStore,
|
|
32
|
-
updater: any[] | Record<string, any> | ((current: any) => any)
|
|
33
|
-
) => {
|
|
34
|
-
formResponsesSignal.value = {
|
|
35
|
-
...formResponsesSignal.value,
|
|
36
|
-
[key]:
|
|
37
|
-
typeof updater === "function"
|
|
38
|
-
? updater(formResponsesSignal.value[key])
|
|
39
|
-
: updater,
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Helper function to update a question response
|
|
44
|
-
const updateQuestionResponse = (
|
|
45
|
-
questionId: string,
|
|
46
|
-
answerValue: any,
|
|
47
|
-
type: QuestionType,
|
|
48
|
-
error?: string
|
|
49
|
-
) => {
|
|
50
|
-
// Check if answer is empty and should be removed instead
|
|
51
|
-
const isEmpty =
|
|
52
|
-
answerValue === null ||
|
|
53
|
-
answerValue === undefined ||
|
|
54
|
-
answerValue === "" ||
|
|
55
|
-
(Array.isArray(answerValue) && answerValue.length === 0);
|
|
56
|
-
|
|
57
|
-
// If empty and no error, remove the question response instead of updating it
|
|
58
|
-
if (isEmpty && !error) {
|
|
59
|
-
removeQuestionResponse(questionId);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Transform the answer value into the correct format based on question type
|
|
64
|
-
let formattedAnswer: Answer;
|
|
65
|
-
|
|
66
|
-
switch (type) {
|
|
67
|
-
case QuestionTypes.RATING:
|
|
68
|
-
formattedAnswer = { rating: answerValue };
|
|
69
|
-
break;
|
|
70
|
-
case QuestionTypes.NPS:
|
|
71
|
-
formattedAnswer = { nps: answerValue };
|
|
72
|
-
break;
|
|
73
|
-
case QuestionTypes.LONG_TEXT:
|
|
74
|
-
formattedAnswer = { longText: answerValue };
|
|
75
|
-
break;
|
|
76
|
-
case QuestionTypes.SHORT_ANSWER:
|
|
77
|
-
formattedAnswer = { shortAnswer: answerValue };
|
|
78
|
-
break;
|
|
79
|
-
case QuestionTypes.SINGLE_CHOICE:
|
|
80
|
-
formattedAnswer = { singleChoice: answerValue };
|
|
81
|
-
break;
|
|
82
|
-
case QuestionTypes.MULTIPLE_CHOICE_MULTIPLE:
|
|
83
|
-
formattedAnswer = {
|
|
84
|
-
multipleChoiceMultiple: Array.isArray(answerValue)
|
|
85
|
-
? answerValue
|
|
86
|
-
: [answerValue],
|
|
87
|
-
};
|
|
88
|
-
break;
|
|
89
|
-
case QuestionTypes.NESTED_SELECTION:
|
|
90
|
-
formattedAnswer = {
|
|
91
|
-
nestedSelection: Array.isArray(answerValue)
|
|
92
|
-
? answerValue
|
|
93
|
-
: [answerValue],
|
|
94
|
-
};
|
|
95
|
-
break;
|
|
96
|
-
case QuestionTypes.ANNOTATION:
|
|
97
|
-
formattedAnswer = { annotation: answerValue };
|
|
98
|
-
break;
|
|
99
|
-
default:
|
|
100
|
-
// If we can't determine the type, just use the answer as is (assuming it's already correctly formatted)
|
|
101
|
-
formattedAnswer = answerValue as Answer;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
setFormResponses("questions", (questions) => {
|
|
105
|
-
const existingIndex = questions.findIndex(
|
|
106
|
-
(q) => q.questionId === questionId
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
if (existingIndex !== -1) {
|
|
110
|
-
// Update existing response
|
|
111
|
-
return questions.map((q, index) =>
|
|
112
|
-
index === existingIndex
|
|
113
|
-
? { ...q, answer: formattedAnswer, type, error }
|
|
114
|
-
: q
|
|
115
|
-
);
|
|
116
|
-
} else {
|
|
117
|
-
// Add new response
|
|
118
|
-
return [
|
|
119
|
-
...questions,
|
|
120
|
-
{ questionId: questionId, answer: formattedAnswer, type, error },
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
// Emit the event after updating the store
|
|
126
|
-
// surveySDK.emit("question_answered", {
|
|
127
|
-
// feedbackId: "your-feedback-id", // <-- Replace with actual feedbackId from context/store
|
|
128
|
-
// questionId,
|
|
129
|
-
// questionText: getQuestionData(questionId)?.title || "",
|
|
130
|
-
// answer: formattedAnswer,
|
|
131
|
-
// timestamp: Date.now(),
|
|
132
|
-
// });
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// Helper function to set/clear validation errors for a question
|
|
136
|
-
const setQuestionError = (questionId: string, error?: string) => {
|
|
137
|
-
const existingQuestion = getQuestionResponse(questionId);
|
|
138
|
-
if (existingQuestion) {
|
|
139
|
-
// Update the error for existing question
|
|
140
|
-
setFormResponses("questions", (questions) =>
|
|
141
|
-
questions.map((q) => (q.questionId === questionId ? { ...q, error } : q))
|
|
142
|
-
);
|
|
143
|
-
} else if (error) {
|
|
144
|
-
// If there's an error but no existing question response, create one with empty answer
|
|
145
|
-
// This is useful for required fields that haven't been filled out yet
|
|
146
|
-
setFormResponses("questions", (questions) => [
|
|
147
|
-
...questions,
|
|
148
|
-
{
|
|
149
|
-
questionId: questionId,
|
|
150
|
-
answer: {} as any,
|
|
151
|
-
type: QuestionTypes.SHORT_ANSWER,
|
|
152
|
-
error,
|
|
153
|
-
},
|
|
154
|
-
]);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// Helper function to clear all validation errors in the form
|
|
159
|
-
const clearValidationErrors = () => {
|
|
160
|
-
setFormResponses("questions", (questions) =>
|
|
161
|
-
questions.map((q) => ({ ...q, error: undefined }))
|
|
162
|
-
);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
// Helper function to check if there are any validation errors in the form
|
|
166
|
-
const hasValidationErrors = () => {
|
|
167
|
-
return formResponsesSignal.value.questions.some(
|
|
168
|
-
(q) => q.error !== undefined && q.error !== null && q.error !== ""
|
|
169
|
-
);
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
// Helper function to remove a question response
|
|
173
|
-
const removeQuestionResponse = (questionId: string) => {
|
|
174
|
-
setFormResponses("questions", (questions) =>
|
|
175
|
-
questions.filter((q) => q.questionId !== questionId)
|
|
176
|
-
);
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
// Helper function to get a question response
|
|
180
|
-
const getQuestionResponse = (questionId: string) => {
|
|
181
|
-
return formResponsesSignal.value.questions.find(
|
|
182
|
-
(q) => q.questionId === questionId
|
|
183
|
-
);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Helper function to clear all responses
|
|
187
|
-
const clearResponses = () => {
|
|
188
|
-
setFormResponses("questions", []);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// Function to store questions data
|
|
192
|
-
const storeQuestions = (questions: Record<string, CombinedQuestion>) => {
|
|
193
|
-
setFormResponses("questionsData", questions);
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const getQuestionData = (questionId: string): CombinedQuestion | undefined => {
|
|
197
|
-
return formResponsesSignal.value.questionsData[questionId];
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export {
|
|
201
|
-
formResponses,
|
|
202
|
-
updateQuestionResponse,
|
|
203
|
-
getQuestionResponse,
|
|
204
|
-
removeQuestionResponse,
|
|
205
|
-
setQuestionError,
|
|
206
|
-
hasValidationErrors,
|
|
207
|
-
clearValidationErrors,
|
|
208
|
-
clearResponses,
|
|
209
|
-
storeQuestions,
|
|
210
|
-
getQuestionData,
|
|
211
|
-
};
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
//import DeviceDetector from "device-detector-js";
|
|
3
|
-
import { DeviceInfo } from "@encatch/schema";
|
|
4
|
-
import { UAParser } from "ua-parser-js";
|
|
5
|
-
|
|
6
|
-
export const getDeviceInfo = (userAgent: string, deviceId: string): DeviceInfo => {
|
|
7
|
-
const { browser, device, os } = UAParser(userAgent);
|
|
8
|
-
const resolvedDeviceType =
|
|
9
|
-
device?.type ||
|
|
10
|
-
(os?.name?.toLowerCase().includes("windows") ||
|
|
11
|
-
os?.name?.toLowerCase().includes("mac") ||
|
|
12
|
-
os?.name?.toLowerCase().includes("linux")
|
|
13
|
-
? "desktop"
|
|
14
|
-
: "Unknown");
|
|
15
|
-
|
|
16
|
-
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
17
|
-
const theme = isDarkMode ? "dark" : "light";
|
|
18
|
-
|
|
19
|
-
// console.log((new UAParser(userAgent)).getResult());
|
|
20
|
-
return {
|
|
21
|
-
$deviceType: resolvedDeviceType,
|
|
22
|
-
$timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
23
|
-
$theme: theme || "system", // This would need to be determined separately
|
|
24
|
-
$os: os?.name || "Unknown",
|
|
25
|
-
$osVersion: os?.version || "Unknown",
|
|
26
|
-
$appVersion: browser?.version || "Unknown", // This would need to be provided from app context
|
|
27
|
-
$app: browser?.name || "Unknown", // This would need to be provided from app context
|
|
28
|
-
$language: typeof navigator !== "undefined" ? navigator.language : "Unknown",
|
|
29
|
-
$deviceId: deviceId,
|
|
30
|
-
};
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Helper Function to Generate a UUID
|
|
34
|
-
export function generateUUID(): string {
|
|
35
|
-
return crypto.randomUUID();
|
|
36
|
-
}
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Utility functions for duration calculations and frequency eligibility
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { FeedbackFrequencyTracking } from "./feedback-frequency-storage";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Convert duration value and unit to milliseconds
|
|
9
|
-
*/
|
|
10
|
-
export const convertDurationToMs = (
|
|
11
|
-
value: string | number,
|
|
12
|
-
unit: "minutes" | "hours" | "days" | "weeks" | "months" | "years"
|
|
13
|
-
): number => {
|
|
14
|
-
const numValue = typeof value === "string" ? parseInt(value, 10) : value;
|
|
15
|
-
|
|
16
|
-
if (isNaN(numValue) || numValue < 0) {
|
|
17
|
-
console.warn(
|
|
18
|
-
`Invalid duration value: ${value}. Defaulting to 0 milliseconds.`
|
|
19
|
-
);
|
|
20
|
-
return 0;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
switch (unit) {
|
|
24
|
-
case "minutes":
|
|
25
|
-
return numValue * 60 * 1000;
|
|
26
|
-
case "hours":
|
|
27
|
-
return numValue * 60 * 60 * 1000;
|
|
28
|
-
case "days":
|
|
29
|
-
return numValue * 24 * 60 * 60 * 1000;
|
|
30
|
-
case "weeks":
|
|
31
|
-
return numValue * 7 * 24 * 60 * 60 * 1000;
|
|
32
|
-
case "months":
|
|
33
|
-
return numValue * 30 * 24 * 60 * 60 * 1000; // Approximate
|
|
34
|
-
case "years":
|
|
35
|
-
return numValue * 365 * 24 * 60 * 60 * 1000; // Approximate
|
|
36
|
-
default:
|
|
37
|
-
console.error(`Unknown duration unit: ${unit}. Defaulting to 0.`);
|
|
38
|
-
return 0;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Check if a feedback is eligible based on frequency rules
|
|
44
|
-
* @param tracking - The frequency tracking data
|
|
45
|
-
* @param currentTime - Current timestamp in milliseconds
|
|
46
|
-
* @param isManual - Whether this is a manual feedback (only checks date range, bypasses all other rules)
|
|
47
|
-
*/
|
|
48
|
-
export const isEligibleByFrequency = (
|
|
49
|
-
tracking: FeedbackFrequencyTracking,
|
|
50
|
-
currentTime: number = Date.now(),
|
|
51
|
-
isManual: boolean = false
|
|
52
|
-
): boolean => {
|
|
53
|
-
const {
|
|
54
|
-
frequencyConfig,
|
|
55
|
-
lastSubmittedAt,
|
|
56
|
-
lastDismissedAt,
|
|
57
|
-
responseCount,
|
|
58
|
-
} = tracking;
|
|
59
|
-
|
|
60
|
-
// For manual feedbacks: Only check start/stop dates, bypass all other frequency rules
|
|
61
|
-
if (isManual) {
|
|
62
|
-
// Check if within date range
|
|
63
|
-
const startDate = new Date(frequencyConfig.startDate).getTime();
|
|
64
|
-
const stopDate = new Date(frequencyConfig.stopDate).getTime();
|
|
65
|
-
|
|
66
|
-
return currentTime >= startDate && currentTime <= stopDate;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// For non-manual feedbacks: Apply all frequency rules
|
|
70
|
-
|
|
71
|
-
// Check if response limit reached
|
|
72
|
-
const stopCount = parseInt(frequencyConfig.stopWhenResponsesCount, 10);
|
|
73
|
-
|
|
74
|
-
if (stopCount > 0 && responseCount >= stopCount) {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// For non-recurring types (S = one-time), check if already submitted
|
|
79
|
-
if (frequencyConfig.surveyType !== "R") {
|
|
80
|
-
const eligible = responseCount === 0;
|
|
81
|
-
return eligible;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// For recurring type "R"
|
|
85
|
-
const durationMs = convertDurationToMs(
|
|
86
|
-
frequencyConfig.recurringValue,
|
|
87
|
-
frequencyConfig.recurringUnit
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// Determine which timestamp to use based on showOnce
|
|
91
|
-
let referenceTime: number | null = null;
|
|
92
|
-
|
|
93
|
-
if (frequencyConfig.showOnce === "Y") {
|
|
94
|
-
// Use the most recent of submitted or dismissed
|
|
95
|
-
const maxTime = Math.max(lastSubmittedAt || 0, lastDismissedAt || 0);
|
|
96
|
-
referenceTime = maxTime > 0 ? maxTime : null;
|
|
97
|
-
} else {
|
|
98
|
-
// Only use submission time
|
|
99
|
-
referenceTime = lastSubmittedAt;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// If never interacted (or only dismissed when showOnce=N), it's eligible
|
|
103
|
-
if (referenceTime === null) {
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Check if enough time has passed
|
|
108
|
-
const nextEligibleTime = referenceTime + durationMs;
|
|
109
|
-
const eligible = currentTime >= nextEligibleTime;
|
|
110
|
-
|
|
111
|
-
return eligible;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Calculate the next eligible time for a feedback
|
|
116
|
-
* Returns null if already eligible or never interacted
|
|
117
|
-
*/
|
|
118
|
-
export const getNextEligibleTime = (
|
|
119
|
-
tracking: FeedbackFrequencyTracking
|
|
120
|
-
): Date | null => {
|
|
121
|
-
const {
|
|
122
|
-
frequencyConfig,
|
|
123
|
-
lastSubmittedAt,
|
|
124
|
-
lastDismissedAt,
|
|
125
|
-
responseCount,
|
|
126
|
-
} = tracking;
|
|
127
|
-
|
|
128
|
-
// If response limit reached, never eligible again
|
|
129
|
-
const stopCount = parseInt(frequencyConfig.stopWhenResponsesCount, 10);
|
|
130
|
-
if (stopCount > 0 && responseCount >= stopCount) {
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// For non-recurring, if already submitted, never eligible again
|
|
135
|
-
if (frequencyConfig.surveyType !== "R" && responseCount > 0) {
|
|
136
|
-
return null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// If never interacted, eligible now
|
|
140
|
-
let referenceTime: number | null = null;
|
|
141
|
-
if (frequencyConfig.showOnce === "Y") {
|
|
142
|
-
const maxTime = Math.max(lastSubmittedAt || 0, lastDismissedAt || 0);
|
|
143
|
-
referenceTime = maxTime > 0 ? maxTime : null;
|
|
144
|
-
} else {
|
|
145
|
-
referenceTime = lastSubmittedAt;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (referenceTime === null) {
|
|
149
|
-
return null; // Already eligible
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const durationMs = convertDurationToMs(
|
|
153
|
-
frequencyConfig.recurringValue,
|
|
154
|
-
frequencyConfig.recurringUnit
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
return new Date(referenceTime + durationMs);
|
|
158
|
-
};
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage utilities for tracking feedback frequency and eligibility
|
|
3
|
-
* Uses localStorage for persistence across sessions
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Feedback frequency tracking data
|
|
8
|
-
* Matches the actual schema structure from @encatch/schema
|
|
9
|
-
*/
|
|
10
|
-
export interface FeedbackFrequencyTracking {
|
|
11
|
-
feedbackConfigurationId: string;
|
|
12
|
-
lastSubmittedAt: number | null; // timestamp in ms
|
|
13
|
-
lastDismissedAt: number | null; // timestamp when viewed but not submitted
|
|
14
|
-
responseCount: number; // total number of submissions
|
|
15
|
-
frequencyConfig: {
|
|
16
|
-
surveyType: "R" | "S"; // "R" = recurring, "S" = one-time
|
|
17
|
-
recurringValue: string; // e.g., "15"
|
|
18
|
-
recurringUnit: "minutes" | "hours" | "days" | "weeks" | "months" | "years";
|
|
19
|
-
showOnce: "Y" | "N"; // "Y" = show once after dismissed/submitted, "N" = can show again
|
|
20
|
-
stopWhenResponsesCount: number; // e.g., "5" or "0" for unlimited
|
|
21
|
-
startDate: string; // ISO date string
|
|
22
|
-
stopDate: string; // ISO date string
|
|
23
|
-
};
|
|
24
|
-
lastUpdated: number; // timestamp of last update
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Storage key for frequency tracking
|
|
29
|
-
*/
|
|
30
|
-
const FREQUENCY_TRACKING_KEY = "encatch_frequency_tracking";
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Get all frequency tracking data from localStorage
|
|
34
|
-
*/
|
|
35
|
-
export const getFrequencyTracking = (): Record<
|
|
36
|
-
string,
|
|
37
|
-
FeedbackFrequencyTracking
|
|
38
|
-
> => {
|
|
39
|
-
try {
|
|
40
|
-
const stored = localStorage.getItem(FREQUENCY_TRACKING_KEY);
|
|
41
|
-
if (!stored) return {};
|
|
42
|
-
|
|
43
|
-
const parsed = JSON.parse(stored);
|
|
44
|
-
return parsed;
|
|
45
|
-
} catch (error) {
|
|
46
|
-
console.error("[Frequency Storage] Error reading tracking data:", error);
|
|
47
|
-
return {};
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Get frequency tracking for a specific feedback
|
|
53
|
-
*/
|
|
54
|
-
export const getFrequencyTrackingById = (
|
|
55
|
-
feedbackConfigurationId: string
|
|
56
|
-
): FeedbackFrequencyTracking | null => {
|
|
57
|
-
const allTracking = getFrequencyTracking();
|
|
58
|
-
return allTracking[feedbackConfigurationId] || null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Update frequency tracking for a feedback
|
|
63
|
-
* @param tracking - The tracking data to update
|
|
64
|
-
*/
|
|
65
|
-
export const updateFrequencyTracking = (
|
|
66
|
-
tracking: FeedbackFrequencyTracking
|
|
67
|
-
): void => {
|
|
68
|
-
try {
|
|
69
|
-
const allTracking = getFrequencyTracking();
|
|
70
|
-
tracking.lastUpdated = Date.now();
|
|
71
|
-
allTracking[tracking.feedbackConfigurationId] = tracking;
|
|
72
|
-
localStorage.setItem(
|
|
73
|
-
FREQUENCY_TRACKING_KEY,
|
|
74
|
-
JSON.stringify(allTracking)
|
|
75
|
-
);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
console.error("[Frequency Storage] Error updating tracking:", error);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Record a feedback submission
|
|
83
|
-
*/
|
|
84
|
-
export const recordFeedbackSubmission = (
|
|
85
|
-
feedbackConfigurationId: string,
|
|
86
|
-
frequencyConfig: FeedbackFrequencyTracking["frequencyConfig"]
|
|
87
|
-
): void => {
|
|
88
|
-
const existing = getFrequencyTrackingById(feedbackConfigurationId);
|
|
89
|
-
|
|
90
|
-
const newTracking: FeedbackFrequencyTracking = {
|
|
91
|
-
feedbackConfigurationId,
|
|
92
|
-
lastSubmittedAt: Date.now(),
|
|
93
|
-
lastDismissedAt: existing?.lastDismissedAt || null, // Preserve dismissed time
|
|
94
|
-
responseCount: (existing?.responseCount || 0) + 1,
|
|
95
|
-
frequencyConfig,
|
|
96
|
-
lastUpdated: Date.now(),
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
updateFrequencyTracking(newTracking);
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Record a feedback dismissal (viewed but not submitted)
|
|
104
|
-
*/
|
|
105
|
-
export const recordFeedbackDismissal = (
|
|
106
|
-
feedbackConfigurationId: string,
|
|
107
|
-
frequencyConfig: FeedbackFrequencyTracking["frequencyConfig"]
|
|
108
|
-
): void => {
|
|
109
|
-
// Only record dismissal if showOnce is "Y"
|
|
110
|
-
if (frequencyConfig.showOnce !== "Y") {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const existing = getFrequencyTrackingById(feedbackConfigurationId);
|
|
115
|
-
|
|
116
|
-
const newTracking: FeedbackFrequencyTracking = {
|
|
117
|
-
feedbackConfigurationId,
|
|
118
|
-
lastSubmittedAt: existing?.lastSubmittedAt || null,
|
|
119
|
-
lastDismissedAt: Date.now(),
|
|
120
|
-
responseCount: existing?.responseCount || 0,
|
|
121
|
-
frequencyConfig,
|
|
122
|
-
lastUpdated: Date.now(),
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
updateFrequencyTracking(newTracking);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Initialize or update tracking with latest config from server
|
|
130
|
-
* This ensures we always have the latest frequency rules
|
|
131
|
-
*/
|
|
132
|
-
export const initializeOrUpdateTracking = (
|
|
133
|
-
feedbackConfigurationId: string,
|
|
134
|
-
frequencyConfig: FeedbackFrequencyTracking["frequencyConfig"]
|
|
135
|
-
): void => {
|
|
136
|
-
const existing = getFrequencyTrackingById(feedbackConfigurationId);
|
|
137
|
-
|
|
138
|
-
if (existing) {
|
|
139
|
-
// Update existing tracking with latest config
|
|
140
|
-
existing.frequencyConfig = frequencyConfig;
|
|
141
|
-
updateFrequencyTracking(existing);
|
|
142
|
-
} else {
|
|
143
|
-
// Initialize new tracking
|
|
144
|
-
const newTracking: FeedbackFrequencyTracking = {
|
|
145
|
-
feedbackConfigurationId,
|
|
146
|
-
lastSubmittedAt: null,
|
|
147
|
-
lastDismissedAt: null,
|
|
148
|
-
responseCount: 0,
|
|
149
|
-
frequencyConfig,
|
|
150
|
-
lastUpdated: Date.now(),
|
|
151
|
-
};
|
|
152
|
-
updateFrequencyTracking(newTracking);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Clear frequency tracking for a specific feedback
|
|
158
|
-
*/
|
|
159
|
-
export const clearFrequencyTrackingById = (
|
|
160
|
-
feedbackConfigurationId: string
|
|
161
|
-
): void => {
|
|
162
|
-
try {
|
|
163
|
-
const allTracking = getFrequencyTracking();
|
|
164
|
-
delete allTracking[feedbackConfigurationId];
|
|
165
|
-
localStorage.setItem(
|
|
166
|
-
FREQUENCY_TRACKING_KEY,
|
|
167
|
-
JSON.stringify(allTracking)
|
|
168
|
-
);
|
|
169
|
-
} catch (error) {
|
|
170
|
-
console.error(
|
|
171
|
-
"[Frequency Storage] Error clearing tracking by ID:",
|
|
172
|
-
error
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Clear all frequency tracking data
|
|
179
|
-
*/
|
|
180
|
-
export const clearAllFrequencyTracking = (): void => {
|
|
181
|
-
try {
|
|
182
|
-
localStorage.removeItem(FREQUENCY_TRACKING_KEY);
|
|
183
|
-
} catch (error) {
|
|
184
|
-
console.error("[Frequency Storage] Error clearing all tracking:", error);
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Get statistics about tracked feedbacks
|
|
190
|
-
*/
|
|
191
|
-
export const getFrequencyTrackingStats = (): {
|
|
192
|
-
totalTracked: number;
|
|
193
|
-
totalSubmissions: number;
|
|
194
|
-
feedbacksWithResponses: number;
|
|
195
|
-
feedbacksAtLimit: number;
|
|
196
|
-
} => {
|
|
197
|
-
const allTracking = getFrequencyTracking();
|
|
198
|
-
const trackingArray = Object.values(allTracking);
|
|
199
|
-
|
|
200
|
-
return {
|
|
201
|
-
totalTracked: trackingArray.length,
|
|
202
|
-
totalSubmissions: trackingArray.reduce(
|
|
203
|
-
(sum, t) => sum + t.responseCount,
|
|
204
|
-
0
|
|
205
|
-
),
|
|
206
|
-
feedbacksWithResponses: trackingArray.filter((t) => t.responseCount > 0)
|
|
207
|
-
.length,
|
|
208
|
-
feedbacksAtLimit: trackingArray.filter(
|
|
209
|
-
(t) =>
|
|
210
|
-
t.frequencyConfig.stopWhenResponsesCount > 0 &&
|
|
211
|
-
t.responseCount >= t.frequencyConfig.stopWhenResponsesCount
|
|
212
|
-
).length,
|
|
213
|
-
};
|
|
214
|
-
};
|