diff --git a/dashboard/components/dashboard/TopCards.vue b/dashboard/components/dashboard/TopCards.vue index 31bd662..e073502 100644 --- a/dashboard/components/dashboard/TopCards.vue +++ b/dashboard/components/dashboard/TopCards.vue @@ -68,21 +68,47 @@ const avgBouncingRate = computed(() => { return avg.toFixed(2) + ' %'; }) +function weightedAverage(data: number[]): number { + if (data.length === 0) return 0; + + // Compute median + const sortedData = [...data].sort((a, b) => a - b); + const middle = Math.floor(sortedData.length / 2); + const median = sortedData.length % 2 === 0 + ? (sortedData[middle - 1] + sortedData[middle]) / 2 + : sortedData[middle]; + + // Define a threshold (e.g., 3 times the median) to filter out extreme values + const threshold = median * 3; + const filteredData = data.filter(num => num <= threshold); + + if (filteredData.length === 0) return median; // Fallback to median if all are removed + + // Compute weights based on inverse absolute deviation from median + const weights = filteredData.map(num => 1 / (1 + Math.abs(num - median))); + + // Compute weighted sum and sum of weights + const weightedSum = filteredData.reduce((sum, num, i) => sum + num * weights[i], 0); + const sumOfWeights = weights.reduce((sum, weight) => sum + weight, 0); + + return weightedSum / sumOfWeights; +} const avgSessionDuration = computed(() => { if (!sessionsDurationData.data.value) return '0.00 %' const counts = sessionsDurationData.data.value.data - .filter(e => e > 0) + // .filter(e => e > 0) .reduce((a, e) => e + a, 0); - const avg = counts / (Math.max(sessionsDurationData.data.value.data.filter(e => e > 0).length, 1)) / 5; + const avg = weightedAverage(sessionsDurationData.data.value.data); + // counts / (Math.max(sessionsDurationData.data.value.data.length, 1)); let hours = 0; let minutes = 0; let seconds = 0; seconds += avg * 60; - while (seconds > 60) { seconds -= 60; minutes += 1; } - while (minutes > 60) { minutes -= 60; hours += 1; } + while (seconds >= 60) { seconds -= 60; minutes += 1; } + while (minutes >= 60) { minutes -= 60; hours += 1; } return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s` }); diff --git a/dashboard/composables/useProject.ts b/dashboard/composables/useProject.ts index 900a1ba..6fb7b49 100644 --- a/dashboard/composables/useProject.ts +++ b/dashboard/composables/useProject.ts @@ -33,9 +33,9 @@ const guestProjectList = computed(() => { return guestProjectsRequest.data.value; }) -const refreshProjectsList = () => { - projectsRequest.refresh(); - guestProjectsRequest.refresh(); +const refreshProjectsList = async () => { + await projectsRequest.refresh(); + await guestProjectsRequest.refresh(); } const activeProjectId = ref(); diff --git a/dashboard/pages/project_creation.vue b/dashboard/pages/project_creation.vue index f896d59..142b45b 100644 --- a/dashboard/pages/project_creation.vue +++ b/dashboard/pages/project_creation.vue @@ -40,8 +40,10 @@ async function createProject() { await actions.refreshProjectsList(); const newActiveProjectId = projectList.value?.[projectList.value?.length - 1]._id.toString(); + if (newActiveProjectId) { await actions.setActiveProject(newActiveProjectId); + console.log('Set active project', newActiveProjectId); } setPageLayout('dashboard'); diff --git a/dashboard/server/api/project/change_name.post.ts b/dashboard/server/api/project/change_name.post.ts index 8ef9032..6fb0aef 100644 --- a/dashboard/server/api/project/change_name.post.ts +++ b/dashboard/server/api/project/change_name.post.ts @@ -8,7 +8,7 @@ export default defineEventHandler(async event => { const { name } = await readBody(event); - if (name.trim()) return setResponseStatus(event, 400, 'name is required'); + if (name.trim().length == 0) return setResponseStatus(event, 400, 'name is required'); if (name.trim().length < 2) return setResponseStatus(event, 400, 'name too short'); if (name.trim().length > 32) return setResponseStatus(event, 400, 'name too long'); diff --git a/dashboard/server/api/project/members/add.post.ts b/dashboard/server/api/project/members/add.post.ts index ae132ea..aa88ce3 100644 --- a/dashboard/server/api/project/members/add.post.ts +++ b/dashboard/server/api/project/members/add.post.ts @@ -20,7 +20,7 @@ export default defineEventHandler(async event => { } - const link = `http://127.0.0.1:3000/accept_invite?project_id=${project_id.toString()}`; + const link = `https://dashboard.litlyx.com/accept_invite?project_id=${project_id.toString()}`; if (!targetUser) { diff --git a/dashboard/server/api/project/members/list.ts b/dashboard/server/api/project/members/list.ts index 6cd5c1d..3801cd9 100644 --- a/dashboard/server/api/project/members/list.ts +++ b/dashboard/server/api/project/members/list.ts @@ -42,7 +42,7 @@ export default defineEventHandler(async event => { }) for (const member of members) { - const userMember = await UserModel.findById(member.user_id); + const userMember = member.user_id ? await UserModel.findById(member.user_id) : await UserModel.findOne({ email: member.email }); if (!userMember) continue; const permission: TPermission = { diff --git a/dashboard/server/api/timeline/sessions_duration.ts b/dashboard/server/api/timeline/sessions_duration.ts index 059766d..a320f7c 100644 --- a/dashboard/server/api/timeline/sessions_duration.ts +++ b/dashboard/server/api/timeline/sessions_duration.ts @@ -24,6 +24,28 @@ export default defineEventHandler(async event => { customProjection: { count: { $divide: ["$duration", "$count"] } }, + customQueries: [ + { + index: 1, + query: { + "$lookup": { + "from": "visits", + "localField": "session", + "foreignField": "session", + "as": "visits", + "pipeline": [{ "$limit": 1 }] + } + }, + }, + { + index: 2, + query: { + "$match": { + "visits.0": { "$exists": true } + } + } + } + ] }); const timelineFilledMerged = fillAndMergeTimelineAggregationV2(timelineData, slice, from, to); return timelineFilledMerged; diff --git a/dashboard/server/services/TimelineService.ts b/dashboard/server/services/TimelineService.ts index 233782b..5494752 100644 --- a/dashboard/server/services/TimelineService.ts +++ b/dashboard/server/services/TimelineService.ts @@ -20,7 +20,8 @@ export type AdvancedTimelineAggregationOptions = TimelineAggregationOptions & { customGroup?: Record, customProjection?: Record, customIdGroup?: Record, - customAfterMatch?: Record + customAfterMatch?: Record, + customQueries?: { index: number, query: Record }[] } export async function executeAdvancedTimelineAggregation(options: AdvancedTimelineAggregationOptions) { @@ -29,6 +30,7 @@ export async function executeAdvancedTimelineAggregation(options: Advanc options.customGroup = options.customGroup || {}; options.customProjection = options.customProjection || {}; options.customIdGroup = options.customIdGroup || {}; + options.customQueries = options.customQueries || []; const { dateFromParts, granularity } = DateService.getGranularityData(options.slice, '$tmpDate'); if (!dateFromParts) throw Error('Slice is probably not correct'); @@ -103,6 +105,9 @@ export async function executeAdvancedTimelineAggregation(options: Advanc } ] as any[]; + for (const customQuery of options.customQueries) { + aggregation.splice(customQuery.index, 0, customQuery.query); + } if (options.customAfterMatch) aggregation.splice(1, 0, options.customAfterMatch); diff --git a/email/src/services/email.ts b/email/src/services/email.ts index d5e2b8e..636cb30 100644 --- a/email/src/services/email.ts +++ b/email/src/services/email.ts @@ -34,7 +34,7 @@ export class EmailService { static async sendInviteEmailNoAccount(target: string, projectName: string, link: string) { try { const sendSmtpEmail = new SendSmtpEmail(); - sendSmtpEmail.subject = "⚡ Invite - No account"; + sendSmtpEmail.subject = "⚡ Invite on a Litlyx project"; sendSmtpEmail.sender = { "name": "Litlyx", "email": "help@litlyx.com" }; sendSmtpEmail.to = [{ "email": target }]; diff --git a/email/templates/ProjectInviteEmail.ts b/email/templates/ProjectInviteEmail.ts index 1cd025a..77e7bbb 100644 --- a/email/templates/ProjectInviteEmail.ts +++ b/email/templates/ProjectInviteEmail.ts @@ -1,21 +1,102 @@ export const PROJECT_INVITE_EMAIL = ` + - - Thank You for Upgrading Your Litlyx Plan! + Email Confirmation + - - + +
+ litlyx-logo -

Invited by [Project Name] on Litlyx

- -

Best regards,

+

You're invited to the Litlyx project [Project Name]!

+

Join now by clicking the button below.

+ + Join the Project + + + + -

Antonio,

-

CEO @ Litlyx

+
+ + ` \ No newline at end of file diff --git a/email/templates/ProjectInviteEmailNoAccount.ts b/email/templates/ProjectInviteEmailNoAccount.ts index a5d25f9..57a92cf 100644 --- a/email/templates/ProjectInviteEmailNoAccount.ts +++ b/email/templates/ProjectInviteEmailNoAccount.ts @@ -1,21 +1,102 @@ export const PROJECT_INVITE_EMAIL_NO_ACCOUNT = ` + - - Thank You for Upgrading Your Litlyx Plan! + Email Confirmation + - - + +
+ litlyx-logo -

Invited by [Project Name] on Litlyx NO_ACCOUNT

- -

Best regards,

+

You're invited to the Litlyx project [Project Name]!

+

Join now by clicking the button below.

+ + Join the Project + + + + -

Antonio,

-

CEO @ Litlyx

+
+ + ` \ No newline at end of file