@embedreach/components 0.3.31 → 0.3.33
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/chunks/index.js +3123 -1962
- package/dist/chunks/sandbox-loading-screen.js +724 -392
- package/dist/index.d.ts +13 -4
- package/dist/index.umd.js +11 -11
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -20663,27 +20663,22 @@ const getPlatformConfigKey = (platformId) => {
|
|
|
20663
20663
|
const defaultPlatformConfigs = {
|
|
20664
20664
|
default: {
|
|
20665
20665
|
id: "default",
|
|
20666
|
-
name: "
|
|
20666
|
+
name: "ACME SaaS",
|
|
20667
20667
|
brandColors: ["#2563eb", "#059669", "#dc2626", "#9333ea"],
|
|
20668
20668
|
locationNames: ["Manhattan", "Brooklyn", "Queens", "Bronx"],
|
|
20669
20669
|
locationAreas: ["New York, NY", "Brooklyn, NY", "Queens, NY", "Bronx, NY"]
|
|
20670
20670
|
},
|
|
20671
20671
|
goose: {
|
|
20672
20672
|
id: "goose",
|
|
20673
|
-
name: "
|
|
20673
|
+
name: "Paws Pet Hotel",
|
|
20674
20674
|
brandColors: ["#f59e0b", "#dc2626", "#059669", "#7c3aed"],
|
|
20675
20675
|
locationNames: [
|
|
20676
|
-
"
|
|
20677
|
-
"
|
|
20678
|
-
"
|
|
20679
|
-
"
|
|
20676
|
+
"Paws Pet Hotel Manhattan",
|
|
20677
|
+
"Paws Pet Hotel Brooklyn",
|
|
20678
|
+
"Paws Pet Hotel Queens",
|
|
20679
|
+
"Paws Pet Hotel Bronx"
|
|
20680
20680
|
],
|
|
20681
|
-
locationAreas: [
|
|
20682
|
-
"San Francisco, CA",
|
|
20683
|
-
"Oakland, CA",
|
|
20684
|
-
"Berkeley, CA",
|
|
20685
|
-
"Palo Alto, CA"
|
|
20686
|
-
]
|
|
20681
|
+
locationAreas: ["Manhattan, NY", "Brooklyn, NY", "Queens, NY", "Bronx, NY"]
|
|
20687
20682
|
},
|
|
20688
20683
|
renterra: {
|
|
20689
20684
|
id: "renterra",
|
|
@@ -20710,7 +20705,6 @@ const getPlatformConfig = (platformId) => {
|
|
|
20710
20705
|
return defaultPlatformConfigs[configKey];
|
|
20711
20706
|
};
|
|
20712
20707
|
const createGbpConnectionFactory = (platformConfig, businessId, status = "connected") => {
|
|
20713
|
-
const accountName = `${platformConfig.name} GBP Account`;
|
|
20714
20708
|
const locations = Array.from(
|
|
20715
20709
|
{ length: Math.min(platformConfig.locationNames.length, 4) },
|
|
20716
20710
|
(_2, index) => ({
|
|
@@ -20734,12 +20728,15 @@ const createGbpConnectionFactory = (platformConfig, businessId, status = "connec
|
|
|
20734
20728
|
platform: "gbp",
|
|
20735
20729
|
selectedAccount: {
|
|
20736
20730
|
name: "accounts/123456789",
|
|
20737
|
-
accountName,
|
|
20731
|
+
accountName: platformConfig.name,
|
|
20738
20732
|
permissionLevel: "OWNER_LEVEL",
|
|
20739
20733
|
verificationState: "VERIFIED",
|
|
20740
20734
|
locationCount: locations.length
|
|
20741
20735
|
},
|
|
20742
|
-
selectedLocations: locations
|
|
20736
|
+
selectedLocations: locations.map((location2) => ({
|
|
20737
|
+
...location2,
|
|
20738
|
+
locationState: location2.locationState
|
|
20739
|
+
}))
|
|
20743
20740
|
},
|
|
20744
20741
|
created_at: f.date.past().toISOString(),
|
|
20745
20742
|
updated_at: f.date.recent().toISOString()
|
|
@@ -20760,7 +20757,7 @@ const createPartnerLocationsFactory = (platformConfig, businessId, platformId, c
|
|
|
20760
20757
|
phone: f.phone.number(),
|
|
20761
20758
|
email: f.internet.email(),
|
|
20762
20759
|
website: `https://${platformConfig.name.toLowerCase().replace(/\s+/g, "-")}.com`,
|
|
20763
|
-
googleBusinessProfilePlaceId:
|
|
20760
|
+
googleBusinessProfilePlaceId: `accounts/123456789/locations/${2e3 + index}`,
|
|
20764
20761
|
businessId,
|
|
20765
20762
|
platformId,
|
|
20766
20763
|
createdAt: f.date.past(),
|
|
@@ -20826,12 +20823,11 @@ const createChannelDataFactory = (platformConfig) => {
|
|
|
20826
20823
|
];
|
|
20827
20824
|
return { channelSenders, channelAccounts };
|
|
20828
20825
|
};
|
|
20829
|
-
const createReputationConfigFactory = (platformConfig, businessId, partnerLocations
|
|
20826
|
+
const createReputationConfigFactory = (platformConfig, businessId, partnerLocations) => {
|
|
20830
20827
|
const locationMappings = {};
|
|
20831
|
-
|
|
20832
|
-
|
|
20833
|
-
|
|
20834
|
-
}
|
|
20828
|
+
partnerLocations.forEach((location2) => {
|
|
20829
|
+
locationMappings[location2.externalId] = location2.googleBusinessProfilePlaceId || null;
|
|
20830
|
+
});
|
|
20835
20831
|
return {
|
|
20836
20832
|
id: f.string.uuid(),
|
|
20837
20833
|
businessId,
|
|
@@ -20860,12 +20856,10 @@ const createSandboxReputationData = (platformId, businessId) => {
|
|
|
20860
20856
|
);
|
|
20861
20857
|
const { channelSenders, channelAccounts } = createChannelDataFactory(platformConfig);
|
|
20862
20858
|
const smsApplication = createSmsApplicationFactory();
|
|
20863
|
-
const gbpLocations = gbpConnection?.metadata?.selectedLocations || [];
|
|
20864
20859
|
const reputationConfig = createReputationConfigFactory(
|
|
20865
20860
|
platformConfig,
|
|
20866
20861
|
businessId,
|
|
20867
|
-
partnerLocations
|
|
20868
|
-
gbpLocations
|
|
20862
|
+
partnerLocations
|
|
20869
20863
|
);
|
|
20870
20864
|
return {
|
|
20871
20865
|
gbpConnection,
|
|
@@ -20879,8 +20873,8 @@ const createSandboxReputationData = (platformId, businessId) => {
|
|
|
20879
20873
|
f.seed(42);
|
|
20880
20874
|
const businessConfigs = {
|
|
20881
20875
|
default: {
|
|
20882
|
-
name: "
|
|
20883
|
-
website: "https://
|
|
20876
|
+
name: "ACME SaaS",
|
|
20877
|
+
website: "https://acme-saas.com",
|
|
20884
20878
|
tagline: "Your sandbox environment for testing",
|
|
20885
20879
|
locations: [
|
|
20886
20880
|
{
|
|
@@ -20893,7 +20887,7 @@ const businessConfigs = {
|
|
|
20893
20887
|
}
|
|
20894
20888
|
],
|
|
20895
20889
|
branding: {
|
|
20896
|
-
brandName: "
|
|
20890
|
+
brandName: "ACME SaaS",
|
|
20897
20891
|
primaryColor: "#2563eb",
|
|
20898
20892
|
secondaryColors: ["#059669", "#dc2626", "#9333ea"],
|
|
20899
20893
|
brandTagline: "Your sandbox environment for testing",
|
|
@@ -20907,7 +20901,8 @@ const businessConfigs = {
|
|
|
20907
20901
|
accent: ["#059669", "#dc2626", "#9333ea"],
|
|
20908
20902
|
background: ["#ffffff", "#f8fafc", "#f1f5f9"],
|
|
20909
20903
|
text: ["#1e293b", "#475569", "#64748b"]
|
|
20910
|
-
}
|
|
20904
|
+
},
|
|
20905
|
+
logoUrl: "https://media.licdn.com/dms/image/v2/C4D0BAQEqer-Bku3Feg/company-logo_200_200/company-logo_200_200/0/1631339163505?e=2147483647&v=beta&t=90F79drLbd8Tiz6oOGqrEDPvC7WbP4Dyp0qGTRToPIo"
|
|
20911
20906
|
}
|
|
20912
20907
|
},
|
|
20913
20908
|
goose: {
|
|
@@ -20939,7 +20934,8 @@ const businessConfigs = {
|
|
|
20939
20934
|
accent: ["#dc2626", "#059669", "#7c3aed"],
|
|
20940
20935
|
background: ["#fef3c7", "#fef2f2", "#f0fdf4"],
|
|
20941
20936
|
text: ["#92400e", "#991b1b", "#166534"]
|
|
20942
|
-
}
|
|
20937
|
+
},
|
|
20938
|
+
logoUrl: "https://static.vecteezy.com/system/resources/previews/009/651/674/non_2x/cute-dog-logo-free-vector.jpg"
|
|
20943
20939
|
}
|
|
20944
20940
|
},
|
|
20945
20941
|
renterra: {
|
|
@@ -20971,7 +20967,8 @@ const businessConfigs = {
|
|
|
20971
20967
|
accent: ["#059669", "#dc2626", "#7c2d12"],
|
|
20972
20968
|
background: ["#dbeafe", "#f0fdf4", "#fef2f2"],
|
|
20973
20969
|
text: ["#1e3a8a", "#166534", "#991b1b"]
|
|
20974
|
-
}
|
|
20970
|
+
},
|
|
20971
|
+
logoUrl: "https://static.vecteezy.com/system/resources/previews/043/794/103/non_2x/heavy-equipment-rental-and-service-logo-design-vector.jpg"
|
|
20975
20972
|
}
|
|
20976
20973
|
}
|
|
20977
20974
|
};
|
|
@@ -22578,9 +22575,18 @@ const generateReputationResponsesData = ({
|
|
|
22578
22575
|
}
|
|
22579
22576
|
if (searchTerm && searchTerm.length >= 3) {
|
|
22580
22577
|
const searchLower = searchTerm.toLowerCase();
|
|
22581
|
-
filteredResponses = filteredResponses.filter(
|
|
22582
|
-
|
|
22583
|
-
|
|
22578
|
+
filteredResponses = filteredResponses.filter((response) => {
|
|
22579
|
+
if (response.userName.toLowerCase().includes(searchLower)) {
|
|
22580
|
+
return true;
|
|
22581
|
+
}
|
|
22582
|
+
if (response.content.toLowerCase().includes(searchLower)) {
|
|
22583
|
+
return true;
|
|
22584
|
+
}
|
|
22585
|
+
if (response.replies && "reply_comment" in response.replies && typeof response.replies.reply_comment === "string" && response.replies.reply_comment.toLowerCase().includes(searchLower)) {
|
|
22586
|
+
return true;
|
|
22587
|
+
}
|
|
22588
|
+
return false;
|
|
22589
|
+
});
|
|
22584
22590
|
}
|
|
22585
22591
|
filteredResponses.sort(
|
|
22586
22592
|
(a2, b2) => new Date(b2.createdAt).getTime() - new Date(a2.createdAt).getTime()
|
|
@@ -22637,41 +22643,79 @@ function createGoogleReview(platformConfig, partnerLocations, startDate, endDate
|
|
|
22637
22643
|
]);
|
|
22638
22644
|
const reviewContent = generateReviewContent(platformConfig, rating);
|
|
22639
22645
|
const location2 = partnerLocations.length > 0 ? f.helpers.arrayElement(partnerLocations) : null;
|
|
22646
|
+
const hasReply = f.helpers.weightedArrayElement([
|
|
22647
|
+
{ weight: 30, value: true },
|
|
22648
|
+
// 30% have business replies
|
|
22649
|
+
{ weight: 70, value: false }
|
|
22650
|
+
]);
|
|
22651
|
+
const replyData = hasReply ? {
|
|
22652
|
+
source: "google_business_profile",
|
|
22653
|
+
reply_comment: generateReplyContent(platformConfig, rating),
|
|
22654
|
+
reply_updated_at: f.date.between({
|
|
22655
|
+
from: new Date(
|
|
22656
|
+
f.date.between({ from: startDate, to: endDate }).getTime() + 24 * 60 * 60 * 1e3
|
|
22657
|
+
),
|
|
22658
|
+
// At least 1 day after review
|
|
22659
|
+
to: endDate
|
|
22660
|
+
}).toISOString()
|
|
22661
|
+
} : void 0;
|
|
22662
|
+
const isLinked = f.helpers.weightedArrayElement([
|
|
22663
|
+
{ weight: 30, value: true },
|
|
22664
|
+
// 30% are linked
|
|
22665
|
+
{ weight: 70, value: false }
|
|
22666
|
+
// 70% are not linked
|
|
22667
|
+
]);
|
|
22668
|
+
const userName = isLinked ? f.person.fullName() : "Anonymous";
|
|
22640
22669
|
return {
|
|
22641
22670
|
id: f.string.uuid(),
|
|
22642
22671
|
source: "google",
|
|
22672
|
+
reviewSource: "google_business_profile",
|
|
22643
22673
|
rating,
|
|
22644
|
-
userName
|
|
22645
|
-
userEmail:
|
|
22646
|
-
// In real Google reviews, email might not be available
|
|
22674
|
+
userName,
|
|
22675
|
+
// userEmail: faker.internet.email(), // In real Google reviews, email might not be available
|
|
22647
22676
|
content: reviewContent,
|
|
22648
22677
|
createdAt: f.date.between({ from: startDate, to: endDate }).toISOString(),
|
|
22649
|
-
hasReply
|
|
22650
|
-
{ weight: 30, value: true },
|
|
22651
|
-
// 30% have business replies
|
|
22652
|
-
{ weight: 70, value: false }
|
|
22653
|
-
]),
|
|
22678
|
+
hasReply,
|
|
22654
22679
|
isResponded: false,
|
|
22655
22680
|
// Not applicable for Google reviews
|
|
22656
|
-
|
|
22657
|
-
|
|
22681
|
+
locationId: location2?.externalId || void 0,
|
|
22682
|
+
...replyData ? { replies: replyData } : {},
|
|
22683
|
+
partnerUserId: isLinked ? f.string.uuid() : void 0,
|
|
22684
|
+
reviewExternalId: f.string.alphanumeric(21)
|
|
22658
22685
|
};
|
|
22659
22686
|
}
|
|
22660
22687
|
function createInternalFeedback(platformConfig, partnerLocations, startDate, endDate) {
|
|
22661
22688
|
const rating = f.helpers.weightedArrayElement([
|
|
22662
|
-
{ weight:
|
|
22663
|
-
//
|
|
22664
|
-
{ weight:
|
|
22665
|
-
//
|
|
22689
|
+
{ weight: 15, value: 10 },
|
|
22690
|
+
// 15% - 10 rating (promoters)
|
|
22691
|
+
{ weight: 25, value: 9 },
|
|
22692
|
+
// 25% - 9 rating (promoters)
|
|
22693
|
+
{ weight: 15, value: 8 },
|
|
22694
|
+
// 15% - 8 rating (passives)
|
|
22695
|
+
{ weight: 10, value: 7 },
|
|
22696
|
+
// 10% - 7 rating (passives)
|
|
22697
|
+
{ weight: 8, value: 6 },
|
|
22698
|
+
// 8% - 6 rating (detractors)
|
|
22699
|
+
{ weight: 7, value: 5 },
|
|
22700
|
+
// 7% - 5 rating (detractors)
|
|
22701
|
+
{ weight: 6, value: 4 },
|
|
22702
|
+
// 6% - 4 rating (detractors)
|
|
22703
|
+
{ weight: 5, value: 3 },
|
|
22704
|
+
// 5% - 3 rating (detractors)
|
|
22705
|
+
{ weight: 4, value: 2 },
|
|
22706
|
+
// 4% - 2 rating (detractors)
|
|
22707
|
+
{ weight: 5, value: 1 }
|
|
22708
|
+
// 5% - 1 rating (detractors)
|
|
22666
22709
|
]);
|
|
22667
22710
|
const feedbackContent = generateFeedbackContent(platformConfig, rating);
|
|
22668
22711
|
const location2 = partnerLocations.length > 0 ? f.helpers.arrayElement(partnerLocations) : null;
|
|
22669
22712
|
return {
|
|
22670
22713
|
id: f.string.uuid(),
|
|
22671
22714
|
source: "internal",
|
|
22715
|
+
reviewSource: "internal_feedback",
|
|
22672
22716
|
rating,
|
|
22673
22717
|
userName: f.person.fullName(),
|
|
22674
|
-
userEmail:
|
|
22718
|
+
// userEmail: faker.internet.email(),
|
|
22675
22719
|
content: feedbackContent,
|
|
22676
22720
|
createdAt: f.date.between({ from: startDate, to: endDate }).toISOString(),
|
|
22677
22721
|
hasReply: false,
|
|
@@ -22681,8 +22725,6 @@ function createInternalFeedback(platformConfig, partnerLocations, startDate, end
|
|
|
22681
22725
|
// 40% have customer responses
|
|
22682
22726
|
{ weight: 60, value: false }
|
|
22683
22727
|
]),
|
|
22684
|
-
actionUrl: void 0,
|
|
22685
|
-
// Internal feedback doesn't have external action URLs
|
|
22686
22728
|
locationId: location2?.externalId || void 0
|
|
22687
22729
|
};
|
|
22688
22730
|
}
|
|
@@ -22755,42 +22797,78 @@ function getReviewTemplates(platformId, rating) {
|
|
|
22755
22797
|
if (rating <= 2) return commonNegative;
|
|
22756
22798
|
return commonNeutral;
|
|
22757
22799
|
}
|
|
22800
|
+
function generateReplyContent(platformConfig, rating) {
|
|
22801
|
+
const platformName = platformConfig.name || "Business";
|
|
22802
|
+
const positiveReplies = [
|
|
22803
|
+
`Thank you so much for your kind words! We're thrilled to hear you had a great experience with ${platformName}. We look forward to serving you again soon!`,
|
|
22804
|
+
`We really appreciate your positive feedback! It's wonderful to know that our team at ${platformName} was able to exceed your expectations. Thank you for choosing us!`,
|
|
22805
|
+
`Thank you for taking the time to leave such a wonderful review! We're so glad you enjoyed your experience at ${platformName}. See you again soon!`,
|
|
22806
|
+
`Your review made our day! We're delighted that you had such a positive experience with ${platformName}. Thank you for your support!`
|
|
22807
|
+
];
|
|
22808
|
+
const neutralReplies = [
|
|
22809
|
+
`Thank you for your feedback. We appreciate you taking the time to share your experience with ${platformName}. We're always working to improve our services and would love to hear more about how we can serve you better.`,
|
|
22810
|
+
`We appreciate your honest feedback. At ${platformName}, we're committed to continuous improvement. Please feel free to reach out to us directly if there's anything specific we can address.`,
|
|
22811
|
+
`Thank you for your review. We value your input and are always looking for ways to enhance our customers' experience at ${platformName}. We hope to have the opportunity to serve you better in the future.`
|
|
22812
|
+
];
|
|
22813
|
+
const negativeReplies = [
|
|
22814
|
+
`We sincerely apologize that your experience at ${platformName} did not meet your expectations. Your feedback is very important to us, and we would like the opportunity to make this right. Please contact us directly so we can address your concerns.`,
|
|
22815
|
+
`Thank you for bringing this to our attention. We're sorry to hear about your experience at ${platformName}. We take all feedback seriously and would appreciate the chance to discuss this with you further. Please reach out to us at your convenience.`,
|
|
22816
|
+
`We're truly sorry about your disappointing experience with ${platformName}. This is not the standard of service we strive for. We'd like to learn more about what happened and work towards a resolution. Please contact our team directly.`,
|
|
22817
|
+
`Your feedback is concerning to us, and we apologize for falling short of your expectations at ${platformName}. We'd very much like to understand what went wrong and how we can improve. Please get in touch with us so we can make this right.`
|
|
22818
|
+
];
|
|
22819
|
+
if (rating >= 4) return f.helpers.arrayElement(positiveReplies);
|
|
22820
|
+
if (rating === 3) return f.helpers.arrayElement(neutralReplies);
|
|
22821
|
+
return f.helpers.arrayElement(negativeReplies);
|
|
22822
|
+
}
|
|
22758
22823
|
function getFeedbackTemplates(platformId, rating) {
|
|
22759
|
-
const
|
|
22760
|
-
"I wanted to share my
|
|
22761
|
-
"
|
|
22762
|
-
"
|
|
22824
|
+
const commonPromoters = [
|
|
22825
|
+
"I wanted to share my exceptional experience with {business}. The entire process was smooth and professional. The staff was knowledgeable and took the time to explain everything clearly. I'm extremely satisfied with the service and would definitely return.",
|
|
22826
|
+
"Outstanding service from {business}! From the initial consultation to the final delivery, everything exceeded my expectations. The team was responsive to my questions and made sure I was completely satisfied with the outcome.",
|
|
22827
|
+
"Excellent experience with {business}. The quality of work was exceptional and the customer service was top-notch. I appreciated the attention to detail and the follow-up communication. Highly recommend!",
|
|
22828
|
+
"Absolutely fantastic service! {business} went above and beyond my expectations. Professional, efficient, and genuinely cared about delivering the best possible outcome."
|
|
22763
22829
|
];
|
|
22764
|
-
const
|
|
22830
|
+
const commonPassives = [
|
|
22831
|
+
"My experience with {business} was good overall. The service was completed as requested, with only minor issues along the way. The staff was generally helpful, though response times could be improved. Met my expectations.",
|
|
22832
|
+
"Solid experience with {business}. Some aspects of the service were very good, while others were just okay. The pricing was reasonable, and the outcome was satisfactory. Room for improvement but generally positive.",
|
|
22833
|
+
"Decent service from {business}. The work was completed professionally, though not exceptional. Staff was courteous and the process was straightforward. Would consider using them again."
|
|
22834
|
+
];
|
|
22835
|
+
const commonDetractors = [
|
|
22765
22836
|
"I had several issues with my recent experience at {business}. The initial promise was not met, and when I tried to address my concerns, the customer service was lacking. The quality of service was below what I expected based on the pricing.",
|
|
22766
22837
|
"Unfortunately, my experience with {business} was disappointing. There were delays in service delivery, and the communication was poor throughout the process. I hope they can improve their operations for future customers.",
|
|
22767
|
-
"Not satisfied with the service from {business}. The staff seemed unprepared and the final result didn't match what was discussed initially. For the price paid, I expected much better quality and professionalism."
|
|
22768
|
-
|
|
22769
|
-
const commonNeutral = [
|
|
22770
|
-
"My experience with {business} was average. The service was completed as requested, but there were some minor issues along the way. The staff was generally helpful, though response times could be improved. Overall, it met basic expectations.",
|
|
22771
|
-
"Mixed feelings about {business}. Some aspects of the service were good, while others fell short. The pricing was fair, but the execution could have been better. They have potential but need some improvements."
|
|
22838
|
+
"Not satisfied with the service from {business}. The staff seemed unprepared and the final result didn't match what was discussed initially. For the price paid, I expected much better quality and professionalism.",
|
|
22839
|
+
"Poor experience overall. {business} needs significant improvements in customer service and service delivery. Multiple issues that were not resolved satisfactorily."
|
|
22772
22840
|
];
|
|
22773
22841
|
if (platformId === "goose") {
|
|
22774
|
-
const
|
|
22775
|
-
"My experience at {business} was wonderful. The staff clearly has extensive training in animal care and grooming techniques. My pet was comfortable throughout the entire process, and the grooming results
|
|
22776
|
-
]
|
|
22842
|
+
const petSpaPromoters = [
|
|
22843
|
+
"My experience at {business} was absolutely wonderful. The staff clearly has extensive training in animal care and grooming techniques. My pet was comfortable throughout the entire process, and the grooming results exceeded what I requested. The facility is immaculate and well-organized, and I truly appreciate the exceptional care they take with each animal."
|
|
22844
|
+
];
|
|
22845
|
+
const petSpaPassives = [
|
|
22846
|
+
"Good experience at {business}. The staff was knowledgeable and my pet was handled well. The grooming results were as requested, though the wait time was a bit longer than expected. Overall satisfied with the service."
|
|
22847
|
+
];
|
|
22848
|
+
const petSpaDetractors = [
|
|
22777
22849
|
"I had concerns about my pet's experience at {business}. While the grooming was completed, my animal seemed stressed afterward. The staff could benefit from additional training on handling anxious pets. The facility was clean, but the process could be more gentle and patient."
|
|
22778
22850
|
];
|
|
22779
|
-
if (rating >=
|
|
22780
|
-
if (rating
|
|
22851
|
+
if (rating >= 9) return [...commonPromoters, ...petSpaPromoters];
|
|
22852
|
+
if (rating >= 7) return [...commonPassives, ...petSpaPassives];
|
|
22853
|
+
return [...commonDetractors, ...petSpaDetractors];
|
|
22781
22854
|
}
|
|
22782
22855
|
if (platformId === "renterra") {
|
|
22783
|
-
const
|
|
22784
|
-
"
|
|
22785
|
-
]
|
|
22856
|
+
const rentalPromoters = [
|
|
22857
|
+
"Exceptional equipment rental experience with {business}! The reservation process was seamless, and the equipment was delivered ahead of schedule in perfect working condition. The staff provided comprehensive instructions and was incredibly responsive to our questions throughout the rental period."
|
|
22858
|
+
];
|
|
22859
|
+
const rentalPassives = [
|
|
22860
|
+
"Solid rental experience with {business}. The equipment worked as expected and delivery was on time. Staff was helpful when contacted. Met our needs for the project, though nothing particularly stood out as exceptional."
|
|
22861
|
+
];
|
|
22862
|
+
const rentalDetractors = [
|
|
22786
22863
|
"Had some challenges with my rental from {business}. The equipment arrived later than scheduled, which impacted our project timeline. Additionally, the equipment showed signs of wear that weren't disclosed upfront. Customer service was slow to respond when we reported issues."
|
|
22787
22864
|
];
|
|
22788
|
-
if (rating >=
|
|
22789
|
-
if (rating
|
|
22865
|
+
if (rating >= 9) return [...commonPromoters, ...rentalPromoters];
|
|
22866
|
+
if (rating >= 7) return [...commonPassives, ...rentalPassives];
|
|
22867
|
+
return [...commonDetractors, ...rentalDetractors];
|
|
22790
22868
|
}
|
|
22791
|
-
if (rating >=
|
|
22792
|
-
if (rating
|
|
22793
|
-
return
|
|
22869
|
+
if (rating >= 9) return commonPromoters;
|
|
22870
|
+
if (rating >= 7) return commonPassives;
|
|
22871
|
+
return commonDetractors;
|
|
22794
22872
|
}
|
|
22795
22873
|
const generateReviewAnalyticsData = ({
|
|
22796
22874
|
platformData,
|
|
@@ -22816,36 +22894,55 @@ const generateReviewAnalyticsData = ({
|
|
|
22816
22894
|
false,
|
|
22817
22895
|
isAllTimeQuery || false
|
|
22818
22896
|
);
|
|
22819
|
-
const
|
|
22820
|
-
|
|
22821
|
-
|
|
22822
|
-
|
|
22823
|
-
|
|
22824
|
-
|
|
22825
|
-
|
|
22826
|
-
|
|
22827
|
-
|
|
22828
|
-
|
|
22829
|
-
|
|
22830
|
-
|
|
22831
|
-
|
|
22832
|
-
|
|
22833
|
-
|
|
22834
|
-
|
|
22835
|
-
|
|
22836
|
-
|
|
22837
|
-
|
|
22838
|
-
|
|
22839
|
-
|
|
22897
|
+
const previousPeriodData = generateReviewPeriodData(
|
|
22898
|
+
platformConfig,
|
|
22899
|
+
scalingFactor,
|
|
22900
|
+
true,
|
|
22901
|
+
// isPrevious = true
|
|
22902
|
+
isAllTimeQuery || false
|
|
22903
|
+
);
|
|
22904
|
+
const totalData = generateReviewPeriodData(
|
|
22905
|
+
platformConfig,
|
|
22906
|
+
1,
|
|
22907
|
+
// No scaling for all-time
|
|
22908
|
+
false,
|
|
22909
|
+
true
|
|
22910
|
+
// isAllTime = true
|
|
22911
|
+
);
|
|
22912
|
+
const totalCountChange = currentPeriodData.totalCount - previousPeriodData.totalCount;
|
|
22913
|
+
const totalCountChangePercent = previousPeriodData.totalCount > 0 ? parseFloat(
|
|
22914
|
+
(totalCountChange / previousPeriodData.totalCount * 100).toFixed(1)
|
|
22915
|
+
) : null;
|
|
22916
|
+
const averageRatingChange = parseFloat(
|
|
22917
|
+
(currentPeriodData.averageRating - previousPeriodData.averageRating).toFixed(1)
|
|
22918
|
+
);
|
|
22919
|
+
const averageRatingChangePercent = previousPeriodData.averageRating > 0 ? parseFloat(
|
|
22920
|
+
(averageRatingChange / previousPeriodData.averageRating * 100).toFixed(1)
|
|
22921
|
+
) : 0;
|
|
22922
|
+
const fullResponse = {
|
|
22840
22923
|
reviewSource: "google_business_profile",
|
|
22841
|
-
|
|
22842
|
-
|
|
22843
|
-
|
|
22924
|
+
current: {
|
|
22925
|
+
totalCount: currentPeriodData.totalCount,
|
|
22926
|
+
averageRating: currentPeriodData.averageRating,
|
|
22927
|
+
ratingDistribution: currentPeriodData.ratingDistribution
|
|
22928
|
+
},
|
|
22929
|
+
previous: {
|
|
22930
|
+
totalCount: previousPeriodData.totalCount,
|
|
22931
|
+
averageRating: previousPeriodData.averageRating,
|
|
22932
|
+
ratingDistribution: previousPeriodData.ratingDistribution
|
|
22933
|
+
},
|
|
22934
|
+
total: totalData,
|
|
22935
|
+
comparison: {
|
|
22936
|
+
totalCountChange,
|
|
22937
|
+
totalCountChangePercent,
|
|
22938
|
+
averageRatingChange,
|
|
22939
|
+
averageRatingChangePercent
|
|
22940
|
+
}
|
|
22844
22941
|
};
|
|
22845
|
-
return
|
|
22942
|
+
return fullResponse;
|
|
22846
22943
|
};
|
|
22847
22944
|
function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
|
|
22848
|
-
const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics
|
|
22945
|
+
const baseMetrics = isAllTime ? getAllTimePlatformReviewMetrics(platformConfig) : getBasePlatformReviewMetrics(platformConfig);
|
|
22849
22946
|
const scaledTotalCount = isAllTime ? baseMetrics.totalCount : Math.max(1, Math.round(baseMetrics.totalCount * scalingFactor));
|
|
22850
22947
|
const performanceMultiplier = isPrevious ? 0.9 : 1;
|
|
22851
22948
|
const adjustedTotalCount = Math.round(
|
|
@@ -22853,7 +22950,7 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
|
|
|
22853
22950
|
);
|
|
22854
22951
|
const ratingDelta = isPrevious ? -0.2 : 0;
|
|
22855
22952
|
const adjustedAverageRating = baseMetrics.averageRating + ratingDelta;
|
|
22856
|
-
const ratingDistribution = generateRatingDistribution(
|
|
22953
|
+
const ratingDistribution = generateRatingDistribution$1(
|
|
22857
22954
|
adjustedTotalCount,
|
|
22858
22955
|
adjustedAverageRating
|
|
22859
22956
|
);
|
|
@@ -22863,22 +22960,24 @@ function generateReviewPeriodData(platformConfig, scalingFactor, isPrevious = fa
|
|
|
22863
22960
|
ratingDistribution
|
|
22864
22961
|
};
|
|
22865
22962
|
}
|
|
22866
|
-
function getBasePlatformReviewMetrics
|
|
22963
|
+
function getBasePlatformReviewMetrics(platformConfig) {
|
|
22867
22964
|
const industryMultipliers = {
|
|
22868
22965
|
// Pet spa industry (Goose) - high customer satisfaction
|
|
22869
|
-
"
|
|
22870
|
-
totalCount:
|
|
22871
|
-
//
|
|
22872
|
-
averageRating: 4.
|
|
22966
|
+
"Paws Pet Hotel": {
|
|
22967
|
+
totalCount: 12,
|
|
22968
|
+
// ~12 reviews per 30 days
|
|
22969
|
+
averageRating: 4.5
|
|
22873
22970
|
},
|
|
22874
22971
|
// Equipment rental (Renterra) - moderate review volume
|
|
22875
22972
|
"Tomer's Rentals": {
|
|
22876
|
-
totalCount:
|
|
22973
|
+
totalCount: 8,
|
|
22974
|
+
// ~8 reviews per 30 days
|
|
22877
22975
|
averageRating: 4.2
|
|
22878
22976
|
},
|
|
22879
22977
|
// Default platform
|
|
22880
22978
|
default: {
|
|
22881
|
-
totalCount:
|
|
22979
|
+
totalCount: 10,
|
|
22980
|
+
// ~10 reviews per 30 days
|
|
22882
22981
|
averageRating: 4.3
|
|
22883
22982
|
}
|
|
22884
22983
|
};
|
|
@@ -22888,27 +22987,28 @@ function getBasePlatformReviewMetrics$1(platformConfig) {
|
|
|
22888
22987
|
function getAllTimePlatformReviewMetrics(platformConfig) {
|
|
22889
22988
|
const industryMultipliers = {
|
|
22890
22989
|
// Pet spa industry (Goose) - high customer satisfaction
|
|
22891
|
-
"
|
|
22892
|
-
totalCount:
|
|
22893
|
-
//
|
|
22894
|
-
averageRating: 4.
|
|
22895
|
-
// Slightly lower all-time average
|
|
22990
|
+
"Paws Pet Hotel": {
|
|
22991
|
+
totalCount: 40,
|
|
22992
|
+
// ~100 days of reviews (12 per 30 days)
|
|
22993
|
+
averageRating: 4.4
|
|
22896
22994
|
},
|
|
22897
22995
|
// Equipment rental (Renterra) - moderate review volume
|
|
22898
22996
|
"Tomer's Rentals": {
|
|
22899
|
-
totalCount:
|
|
22997
|
+
totalCount: 27,
|
|
22998
|
+
// ~100 days of reviews (8 per 30 days)
|
|
22900
22999
|
averageRating: 4.1
|
|
22901
23000
|
},
|
|
22902
23001
|
// Default platform
|
|
22903
23002
|
default: {
|
|
22904
|
-
totalCount:
|
|
23003
|
+
totalCount: 33,
|
|
23004
|
+
// ~100 days of reviews (10 per 30 days)
|
|
22905
23005
|
averageRating: 4.2
|
|
22906
23006
|
}
|
|
22907
23007
|
};
|
|
22908
23008
|
const baseKey = platformConfig.name in industryMultipliers ? platformConfig.name : "default";
|
|
22909
23009
|
return industryMultipliers[baseKey];
|
|
22910
23010
|
}
|
|
22911
|
-
function generateRatingDistribution(totalCount, targetAverage) {
|
|
23011
|
+
function generateRatingDistribution$1(totalCount, targetAverage) {
|
|
22912
23012
|
const baseDistribution = {
|
|
22913
23013
|
5: 0.6,
|
|
22914
23014
|
// 60% 5-star
|
|
@@ -22949,28 +23049,6 @@ function adjustDistributionForAverage(baseDistribution, targetAverage) {
|
|
|
22949
23049
|
}
|
|
22950
23050
|
return adjusted;
|
|
22951
23051
|
}
|
|
22952
|
-
function calculateReviewComparison(currentPeriod, previousPeriod) {
|
|
22953
|
-
const totalCountChange = currentPeriod.totalCount - previousPeriod.totalCount;
|
|
22954
|
-
const totalCountChangePercent = previousPeriod.totalCount > 0 ? parseFloat(
|
|
22955
|
-
(totalCountChange / previousPeriod.totalCount * 100).toFixed(1)
|
|
22956
|
-
) : null;
|
|
22957
|
-
const averageRatingChange = currentPeriod.averageRating && previousPeriod.averageRating ? parseFloat(
|
|
22958
|
-
(currentPeriod.averageRating - previousPeriod.averageRating).toFixed(
|
|
22959
|
-
2
|
|
22960
|
-
)
|
|
22961
|
-
) : null;
|
|
22962
|
-
const averageRatingChangePercent = averageRatingChange && previousPeriod.averageRating ? parseFloat(
|
|
22963
|
-
(averageRatingChange / previousPeriod.averageRating * 100).toFixed(
|
|
22964
|
-
1
|
|
22965
|
-
)
|
|
22966
|
-
) : null;
|
|
22967
|
-
return {
|
|
22968
|
-
totalCountChange,
|
|
22969
|
-
totalCountChangePercent,
|
|
22970
|
-
averageRatingChange,
|
|
22971
|
-
averageRatingChangePercent
|
|
22972
|
-
};
|
|
22973
|
-
}
|
|
22974
23052
|
const generateFeedbackAnalyticsData = ({
|
|
22975
23053
|
platformData,
|
|
22976
23054
|
queryParams
|
|
@@ -22995,38 +23073,86 @@ const generateFeedbackAnalyticsData = ({
|
|
|
22995
23073
|
false,
|
|
22996
23074
|
isAllTimeQuery
|
|
22997
23075
|
);
|
|
22998
|
-
|
|
22999
|
-
|
|
23000
|
-
|
|
23001
|
-
|
|
23002
|
-
|
|
23003
|
-
|
|
23004
|
-
|
|
23005
|
-
|
|
23006
|
-
|
|
23007
|
-
|
|
23008
|
-
|
|
23009
|
-
|
|
23010
|
-
|
|
23011
|
-
|
|
23012
|
-
|
|
23013
|
-
|
|
23014
|
-
|
|
23015
|
-
|
|
23016
|
-
|
|
23017
|
-
|
|
23018
|
-
|
|
23019
|
-
|
|
23020
|
-
|
|
23021
|
-
|
|
23022
|
-
|
|
23023
|
-
|
|
23024
|
-
|
|
23076
|
+
const previousPeriodData = generateFeedbackPeriodData(
|
|
23077
|
+
platformConfig,
|
|
23078
|
+
scalingFactor,
|
|
23079
|
+
true,
|
|
23080
|
+
// isPrevious = true
|
|
23081
|
+
isAllTimeQuery
|
|
23082
|
+
);
|
|
23083
|
+
const totalData = generateFeedbackPeriodData(
|
|
23084
|
+
platformConfig,
|
|
23085
|
+
1,
|
|
23086
|
+
// No scaling for all-time
|
|
23087
|
+
false,
|
|
23088
|
+
true
|
|
23089
|
+
// isAllTime = true
|
|
23090
|
+
);
|
|
23091
|
+
const totalSentChange = currentPeriodData.totalSent - previousPeriodData.totalSent;
|
|
23092
|
+
const totalSentChangePercent = previousPeriodData.totalSent > 0 ? parseFloat(
|
|
23093
|
+
(totalSentChange / previousPeriodData.totalSent * 100).toFixed(1)
|
|
23094
|
+
) : null;
|
|
23095
|
+
const npsScoreChange = currentPeriodData.npsScore - previousPeriodData.npsScore;
|
|
23096
|
+
const npsScoreChangePercent = previousPeriodData.npsScore !== 0 ? parseFloat(
|
|
23097
|
+
(npsScoreChange / Math.abs(previousPeriodData.npsScore) * 100).toFixed(1)
|
|
23098
|
+
) : null;
|
|
23099
|
+
const responseRateChange = parseFloat(
|
|
23100
|
+
(currentPeriodData.responseRate - previousPeriodData.responseRate).toFixed(
|
|
23101
|
+
1
|
|
23102
|
+
)
|
|
23103
|
+
);
|
|
23104
|
+
const responseRateChangePercent = previousPeriodData.responseRate > 0 ? parseFloat(
|
|
23105
|
+
(responseRateChange / previousPeriodData.responseRate * 100).toFixed(1)
|
|
23106
|
+
) : 0;
|
|
23107
|
+
const totalRespondedChange = currentPeriodData.totalResponded - previousPeriodData.totalResponded;
|
|
23108
|
+
const totalRespondedChangePercent = previousPeriodData.totalResponded > 0 ? parseFloat(
|
|
23109
|
+
(totalRespondedChange / previousPeriodData.totalResponded * 100).toFixed(1)
|
|
23110
|
+
) : null;
|
|
23111
|
+
const averageRatingChange = parseFloat(
|
|
23112
|
+
(currentPeriodData.averageRating - previousPeriodData.averageRating).toFixed(1)
|
|
23113
|
+
);
|
|
23114
|
+
const averageRatingChangePercent = previousPeriodData.averageRating > 0 ? parseFloat(
|
|
23115
|
+
(averageRatingChange / previousPeriodData.averageRating * 100).toFixed(1)
|
|
23116
|
+
) : 0;
|
|
23117
|
+
const fullResponse = {
|
|
23118
|
+
current: {
|
|
23119
|
+
totalSent: currentPeriodData.totalSent,
|
|
23120
|
+
totalResponded: currentPeriodData.totalResponded,
|
|
23121
|
+
averageRating: currentPeriodData.averageRating,
|
|
23122
|
+
responseRate: currentPeriodData.responseRate,
|
|
23123
|
+
positiveResponses: currentPeriodData.positiveResponses,
|
|
23124
|
+
negativeResponses: currentPeriodData.negativeResponses,
|
|
23125
|
+
passiveResponses: currentPeriodData.passiveResponses,
|
|
23126
|
+
npsScore: currentPeriodData.npsScore
|
|
23127
|
+
},
|
|
23128
|
+
previous: {
|
|
23129
|
+
totalSent: previousPeriodData.totalSent,
|
|
23130
|
+
totalResponded: previousPeriodData.totalResponded,
|
|
23131
|
+
averageRating: previousPeriodData.averageRating,
|
|
23132
|
+
responseRate: previousPeriodData.responseRate,
|
|
23133
|
+
positiveResponses: previousPeriodData.positiveResponses,
|
|
23134
|
+
negativeResponses: previousPeriodData.negativeResponses,
|
|
23135
|
+
passiveResponses: previousPeriodData.passiveResponses,
|
|
23136
|
+
npsScore: previousPeriodData.npsScore
|
|
23137
|
+
},
|
|
23138
|
+
total: totalData,
|
|
23139
|
+
comparison: {
|
|
23140
|
+
totalSentChange,
|
|
23141
|
+
totalSentChangePercent,
|
|
23142
|
+
npsScoreChange,
|
|
23143
|
+
npsScoreChangePercent,
|
|
23144
|
+
responseRateChange,
|
|
23145
|
+
responseRateChangePercent,
|
|
23146
|
+
totalRespondedChange,
|
|
23147
|
+
totalRespondedChangePercent,
|
|
23148
|
+
averageRatingChange,
|
|
23149
|
+
averageRatingChangePercent
|
|
23150
|
+
}
|
|
23025
23151
|
};
|
|
23026
|
-
return
|
|
23152
|
+
return fullResponse;
|
|
23027
23153
|
};
|
|
23028
23154
|
function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious = false, isAllTime = false) {
|
|
23029
|
-
const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics
|
|
23155
|
+
const baseMetrics = isAllTime ? getAllTimePlatformFeedbackMetrics(platformConfig) : getBasePlatformFeedbackMetrics(platformConfig);
|
|
23030
23156
|
const scaledTotalSent = isAllTime ? baseMetrics.totalSent : Math.max(1, Math.round(baseMetrics.totalSent * scalingFactor));
|
|
23031
23157
|
const performanceMultiplier = isPrevious ? 0.82 : 1;
|
|
23032
23158
|
const adjustedTotalSent = Math.round(scaledTotalSent * performanceMultiplier);
|
|
@@ -23064,10 +23190,10 @@ function generateFeedbackPeriodData(platformConfig, scalingFactor, isPrevious =
|
|
|
23064
23190
|
// real NPS score
|
|
23065
23191
|
};
|
|
23066
23192
|
}
|
|
23067
|
-
function getBasePlatformFeedbackMetrics
|
|
23193
|
+
function getBasePlatformFeedbackMetrics(platformConfig) {
|
|
23068
23194
|
const industryMultipliers = {
|
|
23069
23195
|
// Pet spa industry (Goose) - high engagement, positive feedback
|
|
23070
|
-
"
|
|
23196
|
+
"Paws Pet Hotel": {
|
|
23071
23197
|
totalSent: 1250,
|
|
23072
23198
|
// Much higher - businesses send way more emails than get reviews
|
|
23073
23199
|
responseRate: 42,
|
|
@@ -23110,7 +23236,7 @@ function getBasePlatformFeedbackMetrics$1(platformConfig) {
|
|
|
23110
23236
|
function getAllTimePlatformFeedbackMetrics(platformConfig) {
|
|
23111
23237
|
const industryMultipliers = {
|
|
23112
23238
|
// Pet spa industry (Goose) - high engagement, positive feedback
|
|
23113
|
-
"
|
|
23239
|
+
"Paws Pet Hotel": {
|
|
23114
23240
|
totalSent: 18500,
|
|
23115
23241
|
// WAY higher - businesses send tons of emails over time
|
|
23116
23242
|
responseRate: 39,
|
|
@@ -23159,48 +23285,6 @@ function calculateAverageFromNPS(promoters, passives, detractors) {
|
|
|
23159
23285
|
const weightedSum = promoters * promoterAverage + passives * passiveAverage + detractors * detractorAverage;
|
|
23160
23286
|
return weightedSum / totalResponses;
|
|
23161
23287
|
}
|
|
23162
|
-
function calculateFeedbackComparison(currentPeriod, previousPeriod) {
|
|
23163
|
-
const totalSentChange = currentPeriod.totalSent - previousPeriod.totalSent;
|
|
23164
|
-
const totalSentChangePercent = previousPeriod.totalSent > 0 ? parseFloat(
|
|
23165
|
-
(totalSentChange / previousPeriod.totalSent * 100).toFixed(1)
|
|
23166
|
-
) : null;
|
|
23167
|
-
const totalRespondedChange = currentPeriod.totalResponded - previousPeriod.totalResponded;
|
|
23168
|
-
const totalRespondedChangePercent = previousPeriod.totalResponded > 0 ? parseFloat(
|
|
23169
|
-
(totalRespondedChange / previousPeriod.totalResponded * 100).toFixed(1)
|
|
23170
|
-
) : null;
|
|
23171
|
-
const averageRatingChange = currentPeriod.averageRating && previousPeriod.averageRating ? parseFloat(
|
|
23172
|
-
(currentPeriod.averageRating - previousPeriod.averageRating).toFixed(
|
|
23173
|
-
2
|
|
23174
|
-
)
|
|
23175
|
-
) : null;
|
|
23176
|
-
const averageRatingChangePercent = averageRatingChange && previousPeriod.averageRating ? parseFloat(
|
|
23177
|
-
(averageRatingChange / previousPeriod.averageRating * 100).toFixed(
|
|
23178
|
-
1
|
|
23179
|
-
)
|
|
23180
|
-
) : null;
|
|
23181
|
-
const responseRateChange = currentPeriod.responseRate - previousPeriod.responseRate;
|
|
23182
|
-
const responseRateChangePercent = previousPeriod.responseRate > 0 ? parseFloat(
|
|
23183
|
-
(responseRateChange / previousPeriod.responseRate * 100).toFixed(1)
|
|
23184
|
-
) : null;
|
|
23185
|
-
const npsScoreChange = currentPeriod.npsScore - previousPeriod.npsScore;
|
|
23186
|
-
const npsScoreChangePercent = previousPeriod.npsScore !== 0 ? parseFloat(
|
|
23187
|
-
(npsScoreChange / Math.abs(previousPeriod.npsScore) * 100).toFixed(
|
|
23188
|
-
1
|
|
23189
|
-
)
|
|
23190
|
-
) : null;
|
|
23191
|
-
return {
|
|
23192
|
-
totalSentChange,
|
|
23193
|
-
totalSentChangePercent,
|
|
23194
|
-
totalRespondedChange,
|
|
23195
|
-
totalRespondedChangePercent,
|
|
23196
|
-
averageRatingChange,
|
|
23197
|
-
averageRatingChangePercent,
|
|
23198
|
-
responseRateChange: parseFloat(responseRateChange.toFixed(1)),
|
|
23199
|
-
responseRateChangePercent,
|
|
23200
|
-
npsScoreChange,
|
|
23201
|
-
npsScoreChangePercent
|
|
23202
|
-
};
|
|
23203
|
-
}
|
|
23204
23288
|
const generateAuditLogData = ({
|
|
23205
23289
|
startDate,
|
|
23206
23290
|
endDate,
|
|
@@ -23266,24 +23350,27 @@ function generateAuditLogPool(startDate, endDate, count) {
|
|
|
23266
23350
|
const logs = [];
|
|
23267
23351
|
const timeSpan = endDate.getTime() - startDate.getTime();
|
|
23268
23352
|
const eventSequences = [
|
|
23269
|
-
//
|
|
23353
|
+
// Private Feedback flow
|
|
23270
23354
|
{
|
|
23271
23355
|
type: "SURVEY_REQUEST",
|
|
23272
23356
|
channel: "EMAIL",
|
|
23273
23357
|
statuses: ["SENT", "DELIVERED", "OPENED", "COMPLETED"],
|
|
23274
23358
|
messages: [
|
|
23275
|
-
"
|
|
23276
|
-
"
|
|
23277
|
-
"Customer opened
|
|
23278
|
-
"
|
|
23359
|
+
"Private feedback request sent to customer",
|
|
23360
|
+
"Private feedback request delivered to customer",
|
|
23361
|
+
"Customer opened private feedback request",
|
|
23362
|
+
"Private feedback completed by customer"
|
|
23279
23363
|
]
|
|
23280
23364
|
},
|
|
23281
|
-
// Review flow
|
|
23365
|
+
// Google Review flow
|
|
23282
23366
|
{
|
|
23283
23367
|
type: "REVIEW_REQUEST",
|
|
23284
23368
|
channel: "SMS",
|
|
23285
23369
|
statuses: ["SENT", "DELIVERED"],
|
|
23286
|
-
messages: [
|
|
23370
|
+
messages: [
|
|
23371
|
+
"Google review request sent to",
|
|
23372
|
+
"Google review request delivered to"
|
|
23373
|
+
]
|
|
23287
23374
|
},
|
|
23288
23375
|
// Google review flow
|
|
23289
23376
|
{
|
|
@@ -23300,13 +23387,19 @@ function generateAuditLogPool(startDate, endDate, count) {
|
|
|
23300
23387
|
type: "SURVEY_REMINDER",
|
|
23301
23388
|
channel: "EMAIL",
|
|
23302
23389
|
statuses: ["SENT", "DELIVERED"],
|
|
23303
|
-
messages: [
|
|
23390
|
+
messages: [
|
|
23391
|
+
"Private feedback reminder sent to",
|
|
23392
|
+
"Private feedback reminder delivered to"
|
|
23393
|
+
]
|
|
23304
23394
|
},
|
|
23305
23395
|
{
|
|
23306
23396
|
type: "REVIEW_REMINDER",
|
|
23307
23397
|
channel: "SMS",
|
|
23308
23398
|
statuses: ["SENT", "DELIVERED"],
|
|
23309
|
-
messages: [
|
|
23399
|
+
messages: [
|
|
23400
|
+
"Google review reminder sent to",
|
|
23401
|
+
"Google review reminder delivered to"
|
|
23402
|
+
]
|
|
23310
23403
|
}
|
|
23311
23404
|
];
|
|
23312
23405
|
for (let i2 = 0; i2 < count; i2++) {
|
|
@@ -23348,125 +23441,112 @@ const generateReviewTimeSeriesData = ({
|
|
|
23348
23441
|
platformData,
|
|
23349
23442
|
queryParams
|
|
23350
23443
|
}) => {
|
|
23351
|
-
const
|
|
23352
|
-
platformData
|
|
23353
|
-
|
|
23444
|
+
const analyticsData = generateReviewAnalyticsData({
|
|
23445
|
+
platformData,
|
|
23446
|
+
queryParams
|
|
23447
|
+
});
|
|
23354
23448
|
const { requestedDays, startDate, endDate } = parseDateRange(
|
|
23355
23449
|
queryParams.start_date,
|
|
23356
23450
|
queryParams.end_date,
|
|
23357
23451
|
30
|
|
23358
23452
|
);
|
|
23359
|
-
const
|
|
23360
|
-
const basePeriodDays = 30;
|
|
23361
|
-
const scalingFactor = requestedDays / basePeriodDays;
|
|
23362
|
-
const totalScaledReviews = Math.max(
|
|
23363
|
-
1,
|
|
23364
|
-
Math.round(baseMetrics.totalReviews * scalingFactor)
|
|
23365
|
-
);
|
|
23453
|
+
const isAllTimeQuery = !queryParams.start_date || !queryParams.end_date || queryParams.start_date.startsWith("2020-");
|
|
23366
23454
|
let intervalDays = 7;
|
|
23367
23455
|
if (requestedDays <= 30)
|
|
23368
23456
|
intervalDays = 1;
|
|
23369
23457
|
else if (requestedDays <= 90) intervalDays = 3;
|
|
23370
23458
|
const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
|
|
23371
23459
|
const dataPoints = generateReviewTimeSeriesPoints({
|
|
23372
|
-
|
|
23460
|
+
analyticsData,
|
|
23373
23461
|
startDate,
|
|
23374
23462
|
endDate,
|
|
23375
23463
|
numDataPoints,
|
|
23376
|
-
intervalDays
|
|
23464
|
+
intervalDays,
|
|
23465
|
+
isAllTimeQuery
|
|
23377
23466
|
});
|
|
23378
23467
|
return {
|
|
23379
23468
|
data: dataPoints
|
|
23380
23469
|
};
|
|
23381
23470
|
};
|
|
23382
23471
|
function generateReviewTimeSeriesPoints({
|
|
23383
|
-
|
|
23472
|
+
analyticsData,
|
|
23384
23473
|
startDate,
|
|
23385
23474
|
endDate,
|
|
23386
23475
|
numDataPoints,
|
|
23387
|
-
intervalDays
|
|
23476
|
+
intervalDays,
|
|
23477
|
+
isAllTimeQuery
|
|
23388
23478
|
}) {
|
|
23479
|
+
const reviewCounts = distributeReviewsAcrossPoints(
|
|
23480
|
+
analyticsData.current.totalCount,
|
|
23481
|
+
numDataPoints
|
|
23482
|
+
);
|
|
23483
|
+
const targetRating = analyticsData.total.averageRating ?? 4.2;
|
|
23484
|
+
const startingRating = targetRating - 0.3;
|
|
23485
|
+
const endingRating = targetRating + 0.15;
|
|
23389
23486
|
const dataPoints = [];
|
|
23390
23487
|
let currentDate = new Date(startDate);
|
|
23391
|
-
let
|
|
23392
|
-
|
|
23393
|
-
const
|
|
23394
|
-
const
|
|
23395
|
-
const
|
|
23488
|
+
let runningReviewCount = 0;
|
|
23489
|
+
let runningWeightedSum = 0;
|
|
23490
|
+
const targetEndCumulative = targetRating;
|
|
23491
|
+
const ratingChange = analyticsData.comparison.averageRatingChange ?? 0;
|
|
23492
|
+
const targetStartCumulative = targetRating - ratingChange;
|
|
23396
23493
|
for (let i2 = 0; i2 < numDataPoints; i2++) {
|
|
23397
23494
|
const pointDateStr = currentDate.toISOString().split("T")[0];
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
|
|
23403
|
-
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23495
|
+
const currentPointReviews = reviewCounts[i2];
|
|
23496
|
+
const progress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
|
|
23497
|
+
let currentPointRating = startingRating + (endingRating - startingRating) * Math.pow(progress, 1.3);
|
|
23498
|
+
currentPointRating += (Math.random() - 0.5) * 0.08;
|
|
23499
|
+
currentPointRating = Math.max(1, Math.min(5, currentPointRating));
|
|
23500
|
+
let cumulativeAverageRating;
|
|
23501
|
+
if (isAllTimeQuery) {
|
|
23502
|
+
runningReviewCount += currentPointReviews;
|
|
23503
|
+
runningWeightedSum += currentPointReviews * currentPointRating;
|
|
23504
|
+
cumulativeAverageRating = runningReviewCount > 0 ? parseFloat((runningWeightedSum / runningReviewCount).toFixed(2)) : null;
|
|
23407
23505
|
} else {
|
|
23408
|
-
|
|
23409
|
-
|
|
23506
|
+
const cumulativeProgress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
|
|
23507
|
+
cumulativeAverageRating = parseFloat(
|
|
23508
|
+
(targetStartCumulative + (targetEndCumulative - targetStartCumulative) * cumulativeProgress).toFixed(2)
|
|
23509
|
+
);
|
|
23410
23510
|
}
|
|
23411
|
-
currentPointReviews = Math.max(0, currentPointReviews);
|
|
23412
|
-
currentPointRating = Math.max(1, Math.min(5, currentPointRating));
|
|
23413
|
-
cumulativeReviews += currentPointReviews;
|
|
23414
23511
|
const { promoterCount, detractorCount } = calculatePromoterDetractorSplit(
|
|
23415
23512
|
currentPointReviews,
|
|
23416
23513
|
currentPointRating
|
|
23417
23514
|
);
|
|
23515
|
+
const ratingDistribution = generateRatingDistribution(
|
|
23516
|
+
currentPointReviews,
|
|
23517
|
+
currentPointRating
|
|
23518
|
+
);
|
|
23418
23519
|
dataPoints.push({
|
|
23419
23520
|
date: pointDateStr,
|
|
23420
23521
|
totalReviews: currentPointReviews,
|
|
23421
23522
|
averageRating: currentPointReviews > 0 ? parseFloat(currentPointRating.toFixed(1)) : null,
|
|
23523
|
+
cumulativeAverageRating,
|
|
23422
23524
|
promoterCount,
|
|
23423
|
-
detractorCount
|
|
23525
|
+
detractorCount,
|
|
23526
|
+
ratingDistribution
|
|
23424
23527
|
});
|
|
23425
23528
|
currentDate.setDate(currentDate.getDate() + intervalDays);
|
|
23426
23529
|
if (currentDate > endDate && i2 < numDataPoints - 1) {
|
|
23427
23530
|
currentDate = new Date(endDate);
|
|
23428
|
-
if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
|
|
23429
|
-
break;
|
|
23430
|
-
}
|
|
23431
23531
|
}
|
|
23432
23532
|
}
|
|
23433
23533
|
return dataPoints;
|
|
23434
23534
|
}
|
|
23435
|
-
function
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
averageRating: 4.2,
|
|
23451
|
-
promoterRate: 0.55,
|
|
23452
|
-
// 55% 5-star
|
|
23453
|
-
detractorRate: 0.2
|
|
23454
|
-
// 20% 1-3 star
|
|
23455
|
-
},
|
|
23456
|
-
// Default platform
|
|
23457
|
-
default: {
|
|
23458
|
-
totalReviews: 95,
|
|
23459
|
-
// Match all-time analytics exactly
|
|
23460
|
-
averageRating: 4.2,
|
|
23461
|
-
// Match all-time analytics exactly
|
|
23462
|
-
promoterRate: 0.6,
|
|
23463
|
-
// 60% 5-star
|
|
23464
|
-
detractorRate: 0.18
|
|
23465
|
-
// 18% 1-3 star
|
|
23466
|
-
}
|
|
23467
|
-
};
|
|
23468
|
-
const baseKey = platformConfig.name in industryMetrics ? platformConfig.name : "default";
|
|
23469
|
-
return industryMetrics[baseKey];
|
|
23535
|
+
function distributeReviewsAcrossPoints(totalReviews, numDataPoints) {
|
|
23536
|
+
if (numDataPoints === 0) return [];
|
|
23537
|
+
if (numDataPoints === 1) return [totalReviews];
|
|
23538
|
+
const counts = [];
|
|
23539
|
+
let remainingReviews = totalReviews;
|
|
23540
|
+
const basePerPoint = totalReviews / numDataPoints;
|
|
23541
|
+
for (let i2 = 0; i2 < numDataPoints - 1; i2++) {
|
|
23542
|
+
const variation = 0.7 + Math.random() * 0.6;
|
|
23543
|
+
const count = Math.round(basePerPoint * variation);
|
|
23544
|
+
const actualCount = Math.max(0, Math.min(count, remainingReviews));
|
|
23545
|
+
counts.push(actualCount);
|
|
23546
|
+
remainingReviews -= actualCount;
|
|
23547
|
+
}
|
|
23548
|
+
counts.push(Math.max(0, remainingReviews));
|
|
23549
|
+
return counts;
|
|
23470
23550
|
}
|
|
23471
23551
|
function calculatePromoterDetractorSplit(totalReviews, averageRating) {
|
|
23472
23552
|
if (totalReviews === 0) {
|
|
@@ -23493,33 +23573,80 @@ function calculatePromoterDetractorSplit(totalReviews, averageRating) {
|
|
|
23493
23573
|
const detractorCount = Math.round(totalReviews * detractorRate);
|
|
23494
23574
|
return { promoterCount, detractorCount };
|
|
23495
23575
|
}
|
|
23576
|
+
function generateRatingDistribution(totalReviews, averageRating) {
|
|
23577
|
+
if (totalReviews === 0) {
|
|
23578
|
+
return { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
23579
|
+
}
|
|
23580
|
+
let distribution = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };
|
|
23581
|
+
if (averageRating >= 4.5) {
|
|
23582
|
+
distribution[5] = Math.round(totalReviews * 0.6);
|
|
23583
|
+
distribution[4] = Math.round(totalReviews * 0.3);
|
|
23584
|
+
distribution[3] = Math.round(totalReviews * 0.08);
|
|
23585
|
+
distribution[2] = Math.round(totalReviews * 0.015);
|
|
23586
|
+
distribution[1] = Math.round(totalReviews * 5e-3);
|
|
23587
|
+
} else if (averageRating >= 4) {
|
|
23588
|
+
distribution[5] = Math.round(totalReviews * 0.45);
|
|
23589
|
+
distribution[4] = Math.round(totalReviews * 0.35);
|
|
23590
|
+
distribution[3] = Math.round(totalReviews * 0.15);
|
|
23591
|
+
distribution[2] = Math.round(totalReviews * 0.04);
|
|
23592
|
+
distribution[1] = Math.round(totalReviews * 0.01);
|
|
23593
|
+
} else if (averageRating >= 3.5) {
|
|
23594
|
+
distribution[5] = Math.round(totalReviews * 0.3);
|
|
23595
|
+
distribution[4] = Math.round(totalReviews * 0.35);
|
|
23596
|
+
distribution[3] = Math.round(totalReviews * 0.25);
|
|
23597
|
+
distribution[2] = Math.round(totalReviews * 0.07);
|
|
23598
|
+
distribution[1] = Math.round(totalReviews * 0.03);
|
|
23599
|
+
} else {
|
|
23600
|
+
distribution[5] = Math.round(totalReviews * 0.15);
|
|
23601
|
+
distribution[4] = Math.round(totalReviews * 0.25);
|
|
23602
|
+
distribution[3] = Math.round(totalReviews * 0.35);
|
|
23603
|
+
distribution[2] = Math.round(totalReviews * 0.15);
|
|
23604
|
+
distribution[1] = Math.round(totalReviews * 0.1);
|
|
23605
|
+
}
|
|
23606
|
+
Object.keys(distribution).forEach((rating) => {
|
|
23607
|
+
const key = parseInt(rating);
|
|
23608
|
+
const variation = Math.round(
|
|
23609
|
+
distribution[key] * (Math.random() - 0.5) * 0.2
|
|
23610
|
+
);
|
|
23611
|
+
distribution[key] = Math.max(0, distribution[key] + variation);
|
|
23612
|
+
});
|
|
23613
|
+
const currentTotal = Object.values(distribution).reduce(
|
|
23614
|
+
(sum, count) => sum + count,
|
|
23615
|
+
0
|
|
23616
|
+
);
|
|
23617
|
+
const difference = totalReviews - currentTotal;
|
|
23618
|
+
if (difference !== 0) {
|
|
23619
|
+
const mostCommonRating = Object.entries(distribution).reduce(
|
|
23620
|
+
(max, [rating, count]) => count > distribution[max] ? rating : max,
|
|
23621
|
+
"5"
|
|
23622
|
+
);
|
|
23623
|
+
distribution[mostCommonRating] = Math.max(
|
|
23624
|
+
0,
|
|
23625
|
+
distribution[mostCommonRating] + difference
|
|
23626
|
+
);
|
|
23627
|
+
}
|
|
23628
|
+
return distribution;
|
|
23629
|
+
}
|
|
23496
23630
|
const generateFeedbackTimeSeriesData = ({
|
|
23497
23631
|
platformData,
|
|
23498
23632
|
queryParams
|
|
23499
23633
|
}) => {
|
|
23500
|
-
const
|
|
23501
|
-
platformData
|
|
23502
|
-
|
|
23634
|
+
const analyticsData = generateFeedbackAnalyticsData({
|
|
23635
|
+
platformData,
|
|
23636
|
+
queryParams
|
|
23637
|
+
});
|
|
23503
23638
|
const { requestedDays, startDate, endDate } = parseDateRange(
|
|
23504
23639
|
queryParams.start_date,
|
|
23505
23640
|
queryParams.end_date,
|
|
23506
23641
|
30
|
|
23507
23642
|
);
|
|
23508
|
-
const baseMetrics = getBasePlatformFeedbackMetrics(platformConfig);
|
|
23509
|
-
const basePeriodDays = 30;
|
|
23510
|
-
const scalingFactor = requestedDays / basePeriodDays;
|
|
23511
|
-
const totalScaledSent = Math.max(
|
|
23512
|
-
1,
|
|
23513
|
-
Math.round(baseMetrics.totalSent * scalingFactor)
|
|
23514
|
-
);
|
|
23515
23643
|
let intervalDays = 7;
|
|
23516
23644
|
if (requestedDays <= 30)
|
|
23517
23645
|
intervalDays = 1;
|
|
23518
23646
|
else if (requestedDays <= 90) intervalDays = 3;
|
|
23519
23647
|
const numDataPoints = Math.max(1, Math.ceil(requestedDays / intervalDays));
|
|
23520
23648
|
const dataPoints = generateFeedbackTimeSeriesPoints({
|
|
23521
|
-
|
|
23522
|
-
baseMetrics,
|
|
23649
|
+
analyticsData,
|
|
23523
23650
|
startDate,
|
|
23524
23651
|
endDate,
|
|
23525
23652
|
numDataPoints,
|
|
@@ -23529,52 +23656,54 @@ const generateFeedbackTimeSeriesData = ({
|
|
|
23529
23656
|
data: dataPoints
|
|
23530
23657
|
};
|
|
23531
23658
|
};
|
|
23659
|
+
function distributeFeedbackAcrossPoints(total, numDataPoints) {
|
|
23660
|
+
if (numDataPoints === 0) return [];
|
|
23661
|
+
if (numDataPoints === 1) return [total];
|
|
23662
|
+
const counts = [];
|
|
23663
|
+
let remaining = total;
|
|
23664
|
+
const basePerPoint = total / numDataPoints;
|
|
23665
|
+
for (let i2 = 0; i2 < numDataPoints - 1; i2++) {
|
|
23666
|
+
const variation = 0.7 + Math.random() * 0.6;
|
|
23667
|
+
const count = Math.round(basePerPoint * variation);
|
|
23668
|
+
const actualCount = Math.max(0, Math.min(count, remaining));
|
|
23669
|
+
counts.push(actualCount);
|
|
23670
|
+
remaining -= actualCount;
|
|
23671
|
+
}
|
|
23672
|
+
counts.push(Math.max(0, remaining));
|
|
23673
|
+
return counts;
|
|
23674
|
+
}
|
|
23532
23675
|
function generateFeedbackTimeSeriesPoints({
|
|
23533
|
-
|
|
23534
|
-
baseMetrics,
|
|
23676
|
+
analyticsData,
|
|
23535
23677
|
startDate,
|
|
23536
23678
|
endDate,
|
|
23537
23679
|
numDataPoints,
|
|
23538
23680
|
intervalDays
|
|
23539
23681
|
}) {
|
|
23682
|
+
const sentCounts = distributeFeedbackAcrossPoints(
|
|
23683
|
+
analyticsData.current.totalSent,
|
|
23684
|
+
numDataPoints
|
|
23685
|
+
);
|
|
23686
|
+
const respondedCounts = distributeFeedbackAcrossPoints(
|
|
23687
|
+
analyticsData.current.totalResponded,
|
|
23688
|
+
numDataPoints
|
|
23689
|
+
);
|
|
23690
|
+
const targetEndNPS = analyticsData.total.npsScore;
|
|
23691
|
+
const npsChange = analyticsData.comparison.npsScoreChange ?? 0;
|
|
23692
|
+
const targetStartNPS = targetEndNPS - npsChange;
|
|
23540
23693
|
const dataPoints = [];
|
|
23541
23694
|
let currentDate = new Date(startDate);
|
|
23542
|
-
let cumulativeSent = 0;
|
|
23543
|
-
const basePointSent = totalScaledSent / numDataPoints;
|
|
23544
|
-
const startingResponseRate = baseMetrics.responseRate * 0.75;
|
|
23545
|
-
const endingResponseRate = baseMetrics.responseRate * 1.15;
|
|
23546
|
-
const startingNPS = 30;
|
|
23547
|
-
const endingNPS = 42;
|
|
23548
23695
|
for (let i2 = 0; i2 < numDataPoints; i2++) {
|
|
23549
23696
|
const pointDateStr = currentDate.toISOString().split("T")[0];
|
|
23550
|
-
|
|
23551
|
-
|
|
23552
|
-
|
|
23553
|
-
|
|
23554
|
-
|
|
23555
|
-
basePointSent * (1 + (Math.random() - 0.5) * 0.5)
|
|
23556
|
-
);
|
|
23557
|
-
const progress = i2 / (numDataPoints - 1);
|
|
23558
|
-
currentPointResponseRate = startingResponseRate + (endingResponseRate - startingResponseRate) * Math.pow(progress, 1.3);
|
|
23559
|
-
currentPointNPS = startingNPS + (endingNPS - startingNPS) * Math.pow(progress, 1.2);
|
|
23560
|
-
currentPointResponseRate *= 1 + (Math.random() - 0.5) * 0.1;
|
|
23561
|
-
currentPointNPS *= 1 + (Math.random() - 0.5) * 0.1;
|
|
23562
|
-
} else {
|
|
23563
|
-
currentPointSent = Math.max(0, totalScaledSent - cumulativeSent);
|
|
23564
|
-
currentPointResponseRate = baseMetrics.responseRate;
|
|
23565
|
-
currentPointNPS = 42;
|
|
23566
|
-
}
|
|
23567
|
-
currentPointSent = Math.max(0, currentPointSent);
|
|
23568
|
-
currentPointResponseRate = Math.max(
|
|
23569
|
-
5,
|
|
23570
|
-
Math.min(80, currentPointResponseRate)
|
|
23571
|
-
);
|
|
23697
|
+
const currentPointSent = sentCounts[i2];
|
|
23698
|
+
const currentPointResponded = respondedCounts[i2];
|
|
23699
|
+
const progress = numDataPoints > 1 ? i2 / (numDataPoints - 1) : 1;
|
|
23700
|
+
let currentPointNPS = targetStartNPS + (targetEndNPS - targetStartNPS) * Math.pow(progress, 1.2);
|
|
23701
|
+
currentPointNPS += (Math.random() - 0.5) * 8;
|
|
23572
23702
|
currentPointNPS = Math.max(-100, Math.min(100, currentPointNPS));
|
|
23573
|
-
cumulativeSent += currentPointSent;
|
|
23574
|
-
const currentPointResponded = Math.round(
|
|
23575
|
-
currentPointSent * (currentPointResponseRate / 100)
|
|
23576
|
-
);
|
|
23577
23703
|
const { promoterCount, detractorCount, passiveCount } = calculateNPSBreakdown(currentPointResponded, currentPointNPS);
|
|
23704
|
+
const cumulativeNPS = parseFloat(
|
|
23705
|
+
(targetStartNPS + (targetEndNPS - targetStartNPS) * progress).toFixed(1)
|
|
23706
|
+
);
|
|
23578
23707
|
dataPoints.push({
|
|
23579
23708
|
date: pointDateStr,
|
|
23580
23709
|
totalSent: currentPointSent,
|
|
@@ -23582,56 +23711,16 @@ function generateFeedbackTimeSeriesPoints({
|
|
|
23582
23711
|
promoterCount,
|
|
23583
23712
|
detractorCount,
|
|
23584
23713
|
passiveCount,
|
|
23585
|
-
npsScore:
|
|
23586
|
-
// Use
|
|
23714
|
+
npsScore: cumulativeNPS
|
|
23715
|
+
// Use cumulative NPS for the chart line
|
|
23587
23716
|
});
|
|
23588
23717
|
currentDate.setDate(currentDate.getDate() + intervalDays);
|
|
23589
23718
|
if (currentDate > endDate && i2 < numDataPoints - 1) {
|
|
23590
23719
|
currentDate = new Date(endDate);
|
|
23591
|
-
if (dataPoints.length > 0 && dataPoints[dataPoints.length - 1].date === currentDate.toISOString().split("T")[0]) {
|
|
23592
|
-
break;
|
|
23593
|
-
}
|
|
23594
23720
|
}
|
|
23595
23721
|
}
|
|
23596
23722
|
return dataPoints;
|
|
23597
23723
|
}
|
|
23598
|
-
function getBasePlatformFeedbackMetrics(platformConfig) {
|
|
23599
|
-
const industryMetrics = {
|
|
23600
|
-
// Pet spa industry (Goose) - high engagement, positive feedback
|
|
23601
|
-
"ACME Pet Spa": {
|
|
23602
|
-
totalSent: 1200,
|
|
23603
|
-
// Much higher - realistic email volume
|
|
23604
|
-
responseRate: 44,
|
|
23605
|
-
// 44% response rate
|
|
23606
|
-
targetNPS: 42,
|
|
23607
|
-
// Match the all-time NPS score (42)
|
|
23608
|
-
promoterRate: 0.78
|
|
23609
|
-
// 78% promoters
|
|
23610
|
-
},
|
|
23611
|
-
// Equipment rental (Renterra) - moderate engagement
|
|
23612
|
-
"Tomer's Rentals": {
|
|
23613
|
-
totalSent: 950,
|
|
23614
|
-
responseRate: 37,
|
|
23615
|
-
// 37% response rate
|
|
23616
|
-
targetNPS: 55,
|
|
23617
|
-
// Good NPS for B2B
|
|
23618
|
-
promoterRate: 0.72
|
|
23619
|
-
// 72% promoters
|
|
23620
|
-
},
|
|
23621
|
-
// Default platform
|
|
23622
|
-
default: {
|
|
23623
|
-
totalSent: 1050,
|
|
23624
|
-
responseRate: 40,
|
|
23625
|
-
// 40% response rate
|
|
23626
|
-
targetNPS: 60,
|
|
23627
|
-
// Good overall NPS
|
|
23628
|
-
promoterRate: 0.75
|
|
23629
|
-
// 75% promoters
|
|
23630
|
-
}
|
|
23631
|
-
};
|
|
23632
|
-
const baseKey = platformConfig.name in industryMetrics ? platformConfig.name : "default";
|
|
23633
|
-
return industryMetrics[baseKey];
|
|
23634
|
-
}
|
|
23635
23724
|
function calculateNPSBreakdown(totalResponses, targetNPS) {
|
|
23636
23725
|
if (totalResponses === 0) {
|
|
23637
23726
|
return { promoterCount: 0, detractorCount: 0, passiveCount: 0 };
|
|
@@ -23662,7 +23751,8 @@ function calculateNPSBreakdown(totalResponses, targetNPS) {
|
|
|
23662
23751
|
};
|
|
23663
23752
|
}
|
|
23664
23753
|
const mockStore = {
|
|
23665
|
-
reputationConfig: null
|
|
23754
|
+
reputationConfig: null,
|
|
23755
|
+
partnerLocations: null
|
|
23666
23756
|
};
|
|
23667
23757
|
const reputationHandlers = [
|
|
23668
23758
|
// External OAuth - GBP Connection
|
|
@@ -23677,14 +23767,51 @@ const reputationHandlers = [
|
|
|
23677
23767
|
// Partner Locations
|
|
23678
23768
|
http.get(`${HOSTNAME}/api/partner-locations`, () => {
|
|
23679
23769
|
const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
|
|
23770
|
+
const locations = mockStore.partnerLocations || platformData.reputationData?.partnerLocations || [];
|
|
23680
23771
|
return HttpResponse.json({
|
|
23681
23772
|
success: true,
|
|
23682
23773
|
message: "Success (Sandbox)",
|
|
23683
23774
|
data: {
|
|
23684
|
-
locations
|
|
23775
|
+
locations
|
|
23685
23776
|
}
|
|
23686
23777
|
});
|
|
23687
23778
|
}),
|
|
23779
|
+
// Update Partner Location (for Google Business Profile mapping)
|
|
23780
|
+
http.put(
|
|
23781
|
+
`${HOSTNAME}/api/partner-locations/:id`,
|
|
23782
|
+
async ({ request, params }) => {
|
|
23783
|
+
const locationId = params.id;
|
|
23784
|
+
const body = await request.json();
|
|
23785
|
+
const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
|
|
23786
|
+
if (!mockStore.partnerLocations) {
|
|
23787
|
+
mockStore.partnerLocations = [
|
|
23788
|
+
...platformData.reputationData?.partnerLocations || []
|
|
23789
|
+
];
|
|
23790
|
+
}
|
|
23791
|
+
const locationIndex = mockStore.partnerLocations.findIndex(
|
|
23792
|
+
(loc) => loc.externalId === locationId
|
|
23793
|
+
);
|
|
23794
|
+
if (locationIndex === -1) {
|
|
23795
|
+
return HttpResponse.json(
|
|
23796
|
+
{
|
|
23797
|
+
success: false,
|
|
23798
|
+
message: `Partner location with ID ${locationId} not found (Sandbox)`
|
|
23799
|
+
},
|
|
23800
|
+
{ status: 404 }
|
|
23801
|
+
);
|
|
23802
|
+
}
|
|
23803
|
+
mockStore.partnerLocations[locationIndex] = {
|
|
23804
|
+
...mockStore.partnerLocations[locationIndex],
|
|
23805
|
+
googleBusinessProfilePlaceId: body.googleBusinessProfilePlaceId || null,
|
|
23806
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
23807
|
+
};
|
|
23808
|
+
return HttpResponse.json({
|
|
23809
|
+
success: true,
|
|
23810
|
+
message: "Partner location updated successfully (Sandbox)",
|
|
23811
|
+
data: mockStore.partnerLocations[locationIndex]
|
|
23812
|
+
});
|
|
23813
|
+
}
|
|
23814
|
+
),
|
|
23688
23815
|
// Channel Senders
|
|
23689
23816
|
http.get(`${HOSTNAME}/api/channel/senders`, () => {
|
|
23690
23817
|
const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
|
|
@@ -23710,10 +23837,19 @@ const reputationHandlers = [
|
|
|
23710
23837
|
// Reputation Config
|
|
23711
23838
|
http.get(`${HOSTNAME}/api/reputation/config`, () => {
|
|
23712
23839
|
if (mockStore.reputationConfig) {
|
|
23840
|
+
const currentLocations = mockStore.partnerLocations || getSandboxDataForPlatform(currentSandboxPlatformId).reputationData?.partnerLocations || [];
|
|
23841
|
+
const locationMappings = {};
|
|
23842
|
+
currentLocations.forEach((location2) => {
|
|
23843
|
+
locationMappings[location2.externalId] = location2.googleBusinessProfilePlaceId || null;
|
|
23844
|
+
});
|
|
23713
23845
|
const response2 = {
|
|
23714
23846
|
success: true,
|
|
23715
23847
|
message: "Success (Sandbox)",
|
|
23716
|
-
data:
|
|
23848
|
+
data: {
|
|
23849
|
+
...mockStore.reputationConfig,
|
|
23850
|
+
locationMappings
|
|
23851
|
+
// Always return current mappings from partner locations
|
|
23852
|
+
}
|
|
23717
23853
|
};
|
|
23718
23854
|
return HttpResponse.json(response2);
|
|
23719
23855
|
}
|
|
@@ -23733,7 +23869,7 @@ const reputationHandlers = [
|
|
|
23733
23869
|
if (currentConfig) {
|
|
23734
23870
|
const updatedConfig = {
|
|
23735
23871
|
...currentConfig,
|
|
23736
|
-
|
|
23872
|
+
// Note: locationMappings are now stored in partner_locations table, not here
|
|
23737
23873
|
emailChannelSenderId: body.emailChannelSenderId,
|
|
23738
23874
|
smsChannelSenderId: body.smsChannelSenderId,
|
|
23739
23875
|
// Preserve existing completedOnboardingAt if not provided in update
|
|
@@ -23742,10 +23878,19 @@ const reputationHandlers = [
|
|
|
23742
23878
|
};
|
|
23743
23879
|
mockStore.reputationConfig = updatedConfig;
|
|
23744
23880
|
}
|
|
23881
|
+
const currentLocations = mockStore.partnerLocations || getSandboxDataForPlatform(currentSandboxPlatformId).reputationData?.partnerLocations || [];
|
|
23882
|
+
const locationMappings = {};
|
|
23883
|
+
currentLocations.forEach((location2) => {
|
|
23884
|
+
locationMappings[location2.externalId] = location2.googleBusinessProfilePlaceId || null;
|
|
23885
|
+
});
|
|
23745
23886
|
const response = {
|
|
23746
23887
|
success: true,
|
|
23747
23888
|
message: "Configuration updated successfully (Sandbox)",
|
|
23748
|
-
data:
|
|
23889
|
+
data: {
|
|
23890
|
+
...mockStore.reputationConfig,
|
|
23891
|
+
locationMappings
|
|
23892
|
+
// Include current mappings from partner locations
|
|
23893
|
+
}
|
|
23749
23894
|
};
|
|
23750
23895
|
return HttpResponse.json(response);
|
|
23751
23896
|
}),
|
|
@@ -23895,13 +24040,11 @@ const reputationHandlers = [
|
|
|
23895
24040
|
const startDate = url.searchParams.get("start_date");
|
|
23896
24041
|
const endDate = url.searchParams.get("end_date");
|
|
23897
24042
|
const locationIds = url.searchParams.get("location_ids");
|
|
23898
|
-
const includeComparison = url.searchParams.get("include_comparison") === "true";
|
|
23899
24043
|
const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
|
|
23900
24044
|
const queryParams = {
|
|
23901
24045
|
start_date: startDate || void 0,
|
|
23902
24046
|
end_date: endDate || void 0,
|
|
23903
|
-
location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0
|
|
23904
|
-
include_comparison: includeComparison
|
|
24047
|
+
location_ids: locationIds ? locationIds.split(",").filter(Boolean) : void 0
|
|
23905
24048
|
};
|
|
23906
24049
|
const analyticsData = generateReviewAnalyticsData({
|
|
23907
24050
|
platformData,
|
|
@@ -23949,11 +24092,8 @@ const reputationHandlers = [
|
|
|
23949
24092
|
const url = new URL(request.url);
|
|
23950
24093
|
const startDate = url.searchParams.get("start_date");
|
|
23951
24094
|
const endDate = url.searchParams.get("end_date");
|
|
23952
|
-
const includeComparison = url.searchParams.get("include_comparison") === "true";
|
|
23953
24095
|
const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
|
|
23954
|
-
const queryParams = {
|
|
23955
|
-
include_comparison: includeComparison
|
|
23956
|
-
};
|
|
24096
|
+
const queryParams = {};
|
|
23957
24097
|
if (startDate) {
|
|
23958
24098
|
queryParams.start_date = startDate;
|
|
23959
24099
|
}
|
|
@@ -24023,6 +24163,198 @@ const reputationHandlers = [
|
|
|
24023
24163
|
message: "Success (Sandbox)",
|
|
24024
24164
|
data: auditLogResponse
|
|
24025
24165
|
});
|
|
24166
|
+
}),
|
|
24167
|
+
// Reviews Data Availability
|
|
24168
|
+
http.get(`${HOSTNAME}/api/reviews/data-availability`, () => {
|
|
24169
|
+
const now = /* @__PURE__ */ new Date();
|
|
24170
|
+
const earliestDate = new Date(now);
|
|
24171
|
+
earliestDate.setDate(earliestDate.getDate() - 100);
|
|
24172
|
+
return HttpResponse.json({
|
|
24173
|
+
success: true,
|
|
24174
|
+
message: "Success (Sandbox)",
|
|
24175
|
+
data: {
|
|
24176
|
+
dataAvailableFrom: earliestDate.toISOString()
|
|
24177
|
+
}
|
|
24178
|
+
});
|
|
24179
|
+
}),
|
|
24180
|
+
// Feedback Data Availability
|
|
24181
|
+
http.get(`${HOSTNAME}/api/feedback/data-availability`, () => {
|
|
24182
|
+
const now = /* @__PURE__ */ new Date();
|
|
24183
|
+
const earliestDate = new Date(now);
|
|
24184
|
+
earliestDate.setDate(earliestDate.getDate() - 100);
|
|
24185
|
+
return HttpResponse.json({
|
|
24186
|
+
success: true,
|
|
24187
|
+
message: "Success (Sandbox)",
|
|
24188
|
+
data: {
|
|
24189
|
+
dataAvailableFrom: earliestDate.toISOString()
|
|
24190
|
+
}
|
|
24191
|
+
});
|
|
24192
|
+
}),
|
|
24193
|
+
// Reputation Data Availability
|
|
24194
|
+
http.get(`${HOSTNAME}/api/reputation/data-availability`, () => {
|
|
24195
|
+
const now = /* @__PURE__ */ new Date();
|
|
24196
|
+
const earliestDate = new Date(now);
|
|
24197
|
+
earliestDate.setDate(earliestDate.getDate() - 100);
|
|
24198
|
+
return HttpResponse.json({
|
|
24199
|
+
success: true,
|
|
24200
|
+
message: "Success (Sandbox)",
|
|
24201
|
+
data: {
|
|
24202
|
+
dataAvailableFrom: earliestDate.toISOString()
|
|
24203
|
+
}
|
|
24204
|
+
});
|
|
24205
|
+
}),
|
|
24206
|
+
// Reputation Automation Template
|
|
24207
|
+
http.get(`${HOSTNAME}/api/reputation/reputation-template`, () => {
|
|
24208
|
+
const platformData = getSandboxDataForPlatform(currentSandboxPlatformId);
|
|
24209
|
+
const businessName = platformData.businessesMe?.name || "Our Business";
|
|
24210
|
+
const primaryColor = platformData.businessesMe?.branding?.primaryColor || "#2563eb";
|
|
24211
|
+
const feedbackHtml = `<!DOCTYPE html>
|
|
24212
|
+
<html>
|
|
24213
|
+
<head>
|
|
24214
|
+
<meta charset="UTF-8">
|
|
24215
|
+
<title>Feedback Request</title>
|
|
24216
|
+
<style>
|
|
24217
|
+
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
|
24218
|
+
.header { background: ${primaryColor}; color: white; padding: 30px 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
24219
|
+
.content { background: white; padding: 30px 20px; border-radius: 0 0 8px 8px; }
|
|
24220
|
+
.button { background: ${primaryColor}; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; font-weight: 600; }
|
|
24221
|
+
.rating-scale { display: flex; gap: 8px; justify-content: center; margin: 20px 0; }
|
|
24222
|
+
.rating-box { border: 2px solid #ddd; padding: 12px 16px; border-radius: 4px; font-weight: bold; }
|
|
24223
|
+
</style>
|
|
24224
|
+
</head>
|
|
24225
|
+
<body>
|
|
24226
|
+
<div class="header">
|
|
24227
|
+
<h1>💬 How was your experience?</h1>
|
|
24228
|
+
</div>
|
|
24229
|
+
<div class="content">
|
|
24230
|
+
<p>Hi there! 👋</p>
|
|
24231
|
+
<p>Thank you for choosing <strong>${businessName}</strong>! We hope you had a great experience with us.</p>
|
|
24232
|
+
<p>We'd love to hear your honest feedback. On a scale of 1-10, how would you rate your experience?</p>
|
|
24233
|
+
<div class="rating-scale">
|
|
24234
|
+
<div class="rating-box">1</div>
|
|
24235
|
+
<div class="rating-box">...</div>
|
|
24236
|
+
<div class="rating-box">10</div>
|
|
24237
|
+
</div>
|
|
24238
|
+
<a href="{{feedback_url}}" class="button">Share Your Feedback</a>
|
|
24239
|
+
<p>Your response helps us improve and serve you better.</p>
|
|
24240
|
+
<p>Thank you for your time!</p>
|
|
24241
|
+
<p>Best regards,<br>The ${businessName} Team</p>
|
|
24242
|
+
</div>
|
|
24243
|
+
</body>
|
|
24244
|
+
</html>`;
|
|
24245
|
+
const googleReviewHtml = `<!DOCTYPE html>
|
|
24246
|
+
<html>
|
|
24247
|
+
<head>
|
|
24248
|
+
<meta charset="UTF-8">
|
|
24249
|
+
<title>Review Request</title>
|
|
24250
|
+
<style>
|
|
24251
|
+
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background: #f5f5f5; }
|
|
24252
|
+
.header { background: ${primaryColor}; color: white; padding: 30px 20px; text-align: center; border-radius: 8px 8px 0 0; }
|
|
24253
|
+
.content { background: white; padding: 30px 20px; border-radius: 0 0 8px 8px; }
|
|
24254
|
+
.button { background: ${primaryColor}; color: white; padding: 14px 28px; text-decoration: none; border-radius: 6px; display: inline-block; margin: 20px 0; font-weight: 600; }
|
|
24255
|
+
.stars { color: #fbbf24; font-size: 24px; }
|
|
24256
|
+
</style>
|
|
24257
|
+
</head>
|
|
24258
|
+
<body>
|
|
24259
|
+
<div class="header">
|
|
24260
|
+
<h1>⭐ We'd love your Google review!</h1>
|
|
24261
|
+
</div>
|
|
24262
|
+
<div class="content">
|
|
24263
|
+
<p>Hi there! 👋</p>
|
|
24264
|
+
<p>Thank you so much for your positive feedback about <strong>${businessName}</strong>!</p>
|
|
24265
|
+
<p>Since you had a great experience, we'd be incredibly grateful if you could share your thoughts on Google. It only takes a moment and helps other customers discover us.</p>
|
|
24266
|
+
<div class="stars">⭐ ⭐ ⭐ ⭐ ⭐</div>
|
|
24267
|
+
<a href="{{external_review_url}}" class="button">Leave a Google Review</a>
|
|
24268
|
+
<p>Your support means the world to us and helps our business grow!</p>
|
|
24269
|
+
<p>Thank you so much!</p>
|
|
24270
|
+
<p>Warmly,<br>The ${businessName} Team</p>
|
|
24271
|
+
</div>
|
|
24272
|
+
</body>
|
|
24273
|
+
</html>`;
|
|
24274
|
+
const templates = [
|
|
24275
|
+
{
|
|
24276
|
+
id: "0199110d-eac1-7211-aeb3-1d9d9b60e77b",
|
|
24277
|
+
name: "Private Feedback Request",
|
|
24278
|
+
version: "1.0.0",
|
|
24279
|
+
status: "active",
|
|
24280
|
+
feature: "reputation",
|
|
24281
|
+
visibility: "visible",
|
|
24282
|
+
contents: {
|
|
24283
|
+
name: "reputation-feedback-request",
|
|
24284
|
+
description: "Request private direct feedback from customers",
|
|
24285
|
+
feature: "reputation",
|
|
24286
|
+
triggerMetadata: {
|
|
24287
|
+
cooldownSeconds: 86400,
|
|
24288
|
+
// 1 day after purchase
|
|
24289
|
+
eventFilter: {
|
|
24290
|
+
segment_id: "all_customers"
|
|
24291
|
+
}
|
|
24292
|
+
},
|
|
24293
|
+
actionData: [
|
|
24294
|
+
{
|
|
24295
|
+
type: "send_communication",
|
|
24296
|
+
index: 0,
|
|
24297
|
+
subject: `How was your experience with ${businessName}?`,
|
|
24298
|
+
previewText: "We'd love your honest feedback",
|
|
24299
|
+
compiledHtml: feedbackHtml
|
|
24300
|
+
}
|
|
24301
|
+
]
|
|
24302
|
+
}
|
|
24303
|
+
},
|
|
24304
|
+
{
|
|
24305
|
+
id: "0199110d-eac1-7211-aeb3-1d9d9b60e77c",
|
|
24306
|
+
name: "Google Review Request",
|
|
24307
|
+
version: "1.0.0",
|
|
24308
|
+
status: "active",
|
|
24309
|
+
feature: "reputation",
|
|
24310
|
+
visibility: "visible",
|
|
24311
|
+
contents: {
|
|
24312
|
+
name: "reputation-google-review-request",
|
|
24313
|
+
description: "Request Google review from satisfied customers (9-10 rating)",
|
|
24314
|
+
feature: "reputation",
|
|
24315
|
+
triggerMetadata: {
|
|
24316
|
+
cooldownSeconds: 0,
|
|
24317
|
+
// Sent immediately after positive feedback
|
|
24318
|
+
eventFilter: {
|
|
24319
|
+
segment_id: "satisfied_customers"
|
|
24320
|
+
}
|
|
24321
|
+
},
|
|
24322
|
+
actionData: [
|
|
24323
|
+
{
|
|
24324
|
+
type: "send_communication",
|
|
24325
|
+
index: 0,
|
|
24326
|
+
subject: `Would you leave us a Google review? - ${businessName}`,
|
|
24327
|
+
previewText: "Share your positive experience with others",
|
|
24328
|
+
compiledHtml: googleReviewHtml
|
|
24329
|
+
}
|
|
24330
|
+
]
|
|
24331
|
+
}
|
|
24332
|
+
}
|
|
24333
|
+
];
|
|
24334
|
+
const sortedTemplates = templates.sort(
|
|
24335
|
+
(a2, b2) => a2.name.localeCompare(b2.name, void 0, { sensitivity: "base" })
|
|
24336
|
+
);
|
|
24337
|
+
return HttpResponse.json({
|
|
24338
|
+
success: true,
|
|
24339
|
+
message: "Success (Sandbox)",
|
|
24340
|
+
data: sortedTemplates
|
|
24341
|
+
});
|
|
24342
|
+
}),
|
|
24343
|
+
// Activate Automations
|
|
24344
|
+
http.post(`${HOSTNAME}/api/reputation/activate-automations`, () => {
|
|
24345
|
+
return HttpResponse.json({
|
|
24346
|
+
success: true,
|
|
24347
|
+
message: "Success (Sandbox)",
|
|
24348
|
+
data: {}
|
|
24349
|
+
});
|
|
24350
|
+
}),
|
|
24351
|
+
// Sync Google Reviews
|
|
24352
|
+
http.post(`${HOSTNAME}/api/reputation/sync-google-reviews`, () => {
|
|
24353
|
+
return HttpResponse.json({
|
|
24354
|
+
success: true,
|
|
24355
|
+
message: "Success (Sandbox)",
|
|
24356
|
+
data: {}
|
|
24357
|
+
});
|
|
24026
24358
|
})
|
|
24027
24359
|
];
|
|
24028
24360
|
const getHandlersByFeatures = (features) => {
|