mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
adjust admin dashboard
This commit is contained in:
@@ -4,109 +4,65 @@ import type { AdminProjectsList } from '~/server/api/admin/projects';
|
|||||||
|
|
||||||
definePageMeta({ layout: 'dashboard' });
|
definePageMeta({ layout: 'dashboard' });
|
||||||
|
|
||||||
const { data: projects } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
const { data: projectsAggregatedResponseData } = await useFetch<AdminProjectsList[]>('/api/admin/projects', signHeaders());
|
||||||
const { data: counts } = await useFetch('/api/admin/counts', signHeaders());
|
const { data: counts } = await useFetch('/api/admin/counts', signHeaders());
|
||||||
|
|
||||||
|
|
||||||
type TProjectsGrouped = {
|
|
||||||
user: {
|
|
||||||
name: string,
|
|
||||||
email: string,
|
|
||||||
given_name: string,
|
|
||||||
picture: string,
|
|
||||||
created_at: Date
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
_id: string,
|
|
||||||
premium: boolean,
|
|
||||||
premium_type: number,
|
|
||||||
created_at: Date,
|
|
||||||
project_name: string,
|
|
||||||
total_visits: number,
|
|
||||||
total_events: number,
|
|
||||||
total_sessions: number
|
|
||||||
}[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const projectsGrouped = computed(() => {
|
|
||||||
|
|
||||||
if (!projects.value) return [];
|
|
||||||
|
|
||||||
const result: TProjectsGrouped[] = [];
|
|
||||||
|
|
||||||
for (const project of projects.value) {
|
|
||||||
|
|
||||||
if (!project.user) continue;
|
|
||||||
|
|
||||||
|
|
||||||
const target = result.find(e => e.user.email == project.user.email);
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
|
|
||||||
target.projects.push({
|
|
||||||
_id: project._id,
|
|
||||||
created_at: project.created_at,
|
|
||||||
premium_type: project.premium_type,
|
|
||||||
premium: project.premium,
|
|
||||||
project_name: project.project_name,
|
|
||||||
total_events: project.total_events,
|
|
||||||
total_visits: project.total_visits,
|
|
||||||
total_sessions: project.total_sessions
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
const item: TProjectsGrouped = {
|
|
||||||
user: project.user,
|
|
||||||
projects: [{
|
|
||||||
_id: project._id,
|
|
||||||
created_at: project.created_at,
|
|
||||||
premium: project.premium,
|
|
||||||
premium_type: project.premium_type,
|
|
||||||
project_name: project.project_name,
|
|
||||||
total_events: project.total_events,
|
|
||||||
total_visits: project.total_visits,
|
|
||||||
total_sessions: project.total_sessions
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(item);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
result.sort((sa, sb) => {
|
|
||||||
const ca = sa.projects.reduce((a, e) => a + (e.total_visits + e.total_events), 0);
|
|
||||||
const cb = sb.projects.reduce((a, e) => a + (e.total_visits + e.total_events), 0);
|
|
||||||
return cb - ca;
|
|
||||||
})
|
|
||||||
|
|
||||||
return result;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function onHideClicked() {
|
function onHideClicked() {
|
||||||
isAdminHidden.value = true;
|
isAdminHidden.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const projectsAggregated = computed(() => {
|
||||||
|
return projectsAggregatedResponseData.value?.sort((a, b) => {
|
||||||
|
const sumVisitsA = a.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
||||||
|
const sumVisitsB = b.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0) + (pe.counts?.events || 0), 0);
|
||||||
|
return sumVisitsB - sumVisitsA;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
const premiumCount = computed(() => {
|
const premiumCount = computed(() => {
|
||||||
let premiums = 0;
|
let premiums = 0;
|
||||||
projects.value?.forEach(e => {
|
projectsAggregated.value?.forEach(e => {
|
||||||
if (e.premium) premiums++;
|
e.projects.forEach(p => {
|
||||||
|
if (p.premium) premiums++;
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
return premiums;
|
return premiums;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const activeProjects = computed(() => {
|
||||||
|
let actives = 0;
|
||||||
|
|
||||||
|
projectsAggregated.value?.forEach(e => {
|
||||||
|
e.projects.forEach(p => {
|
||||||
|
if (!p.counts) return;
|
||||||
|
if (!p.counts.updated_at) return;
|
||||||
|
const updated_at = new Date(p.counts.updated_at).getTime();
|
||||||
|
if (updated_at < Date.now() - 1000 * 60 * 60 * 24) return;
|
||||||
|
actives++;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
return actives;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const totalVisits = computed(() => {
|
const totalVisits = computed(() => {
|
||||||
return projects.value?.reduce((a, e) => a + e.total_visits, 0) || 0;
|
return projectsAggregated.value?.reduce((a, e) => {
|
||||||
|
return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.visits || 0), 0);
|
||||||
|
}, 0) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalEvents = computed(() => {
|
const totalEvents = computed(() => {
|
||||||
return projects.value?.reduce((a, e) => a + e.total_events, 0) || 0;
|
return projectsAggregated.value?.reduce((a, e) => {
|
||||||
|
return a + e.projects.reduce((pa, pe) => pa + (pe.counts?.events || 0), 0);
|
||||||
|
}, 0) || 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const details = ref<any>();
|
const details = ref<any>();
|
||||||
const showDetails = ref<boolean>(false);
|
const showDetails = ref<boolean>(false);
|
||||||
async function getProjectDetails(project_id: string) {
|
async function getProjectDetails(project_id: string) {
|
||||||
@@ -118,6 +74,28 @@ async function resetCount(project_id: string) {
|
|||||||
await $fetch(`/api/admin/reset_count?project_id=${project_id}`, signHeaders());
|
await $fetch(`/api/admin/reset_count?project_id=${project_id}`, signHeaders());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function dateDiffDays(a: string) {
|
||||||
|
return (Date.now() - new Date(a).getTime()) / (1000 * 60 * 60 * 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogBg(last_logged_at?: string) {
|
||||||
|
|
||||||
|
const day = 1000 * 60 * 60 * 24;
|
||||||
|
const week = 1000 * 60 * 60 * 24 * 7;
|
||||||
|
|
||||||
|
const lastLoggedAtDate = new Date(last_logged_at || 0);
|
||||||
|
|
||||||
|
if (lastLoggedAtDate.getTime() > Date.now() - day) {
|
||||||
|
return 'bg-green-500'
|
||||||
|
} else if (lastLoggedAtDate.getTime() > Date.now() - week) {
|
||||||
|
return 'bg-yellow-500'
|
||||||
|
} else {
|
||||||
|
return 'bg-red-500'
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -144,7 +122,7 @@ async function resetCount(project_id: string) {
|
|||||||
|
|
||||||
<Card class="p-4">
|
<Card class="p-4">
|
||||||
|
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-2 gap-1">
|
||||||
<div>
|
<div>
|
||||||
Users: {{ counts?.users }}
|
Users: {{ counts?.users }}
|
||||||
</div>
|
</div>
|
||||||
@@ -154,6 +132,10 @@ async function resetCount(project_id: string) {
|
|||||||
<div>
|
<div>
|
||||||
Total visits: {{ formatNumberK(totalVisits) }}
|
Total visits: {{ formatNumberK(totalVisits) }}
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
Active: {{ activeProjects }} |
|
||||||
|
Dead: {{ (counts?.projects || 0) - activeProjects }}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Total events: {{ formatNumberK(totalEvents) }}
|
Total events: {{ formatNumberK(totalEvents) }}
|
||||||
</div>
|
</div>
|
||||||
@@ -162,17 +144,29 @@ async function resetCount(project_id: string) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
||||||
<div v-for="item of projectsGrouped" class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative">
|
<Card>
|
||||||
|
<!-- <USelectMenu></USelectMenu> -->
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div v-for="item of projectsAggregated || []"
|
||||||
|
class="bg-menu p-4 rounded-xl flex flex-col gap-2 w-full relative">
|
||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div> {{ item.user.email }} </div>
|
<div> {{ item.email }} </div>
|
||||||
<div> {{ item.user.name }} </div>
|
<div> {{ item.name }} </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-evenly flex-col lg:flex-row gap-2 lg:gap-0">
|
<div class="flex justify-evenly flex-col lg:grid lg:grid-cols-3 gap-2 lg:gap-4">
|
||||||
|
|
||||||
<div v-for="project of item.projects"
|
<div v-for="project of item.projects"
|
||||||
class="lg:w-[30%] flex flex-col items-center bg-bg p-6 rounded-xl">
|
class="flex relative flex-col items-center bg-bg p-6 rounded-xl">
|
||||||
|
|
||||||
|
<div class="absolute left-2 top-2 flex items-center gap-2">
|
||||||
|
<div :class="getLogBg(project?.counts?.updated_at)" class="h-3 w-3 rounded-full"> </div>
|
||||||
|
<div> {{ dateDiffDays(project?.counts?.updated_at || '0').toFixed(0) }} days </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="font-bold"> {{ project.premium ? 'PREMIUM' : 'FREE' }} </div>
|
<div class="font-bold"> {{ project.premium ? 'PREMIUM' : 'FREE' }} </div>
|
||||||
<div class="text-text-sub/90">
|
<div class="text-text-sub/90">
|
||||||
@@ -181,14 +175,14 @@ async function resetCount(project_id: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="text-ellipsis line-clamp-1"> {{ project.project_name }} </div>
|
<div class="text-ellipsis line-clamp-1"> {{ project.name }} </div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<div> Visits: </div>
|
<div> Visits: </div>
|
||||||
<div> {{ project.total_visits }} </div>
|
<div> {{ formatNumberK(project.counts?.visits || 0) }} </div>
|
||||||
<div> Events: </div>
|
<div> Events: </div>
|
||||||
<div> {{ project.total_events }} </div>
|
<div> {{ formatNumberK(project.counts?.events || 0) }} </div>
|
||||||
<div> Sessions: </div>
|
<div> Sessions: </div>
|
||||||
<div> {{ project.total_sessions }} </div>
|
<div> {{ formatNumberK(project.counts?.sessions || 0) }} </div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4 items-center mt-4">
|
<div class="flex gap-4 items-center mt-4">
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { ProjectModel } from "@schema/ProjectSchema";
|
import { UserModel } from "@schema/UserSchema";
|
||||||
|
|
||||||
export type AdminProjectsList = {
|
export type AdminProjectsList = {
|
||||||
premium: boolean,
|
|
||||||
created_at: Date,
|
|
||||||
project_name: string,
|
|
||||||
premium_type: number,
|
|
||||||
_id: string,
|
_id: string,
|
||||||
user: {
|
|
||||||
name: string,
|
name: string,
|
||||||
email: string,
|
|
||||||
given_name: string,
|
given_name: string,
|
||||||
picture: string,
|
created_at: string,
|
||||||
created_at: Date
|
email: string,
|
||||||
},
|
projects: {
|
||||||
total_visits: number,
|
_id: string,
|
||||||
total_events: number,
|
owner: string,
|
||||||
total_sessions: number
|
name: string,
|
||||||
|
premium: boolean,
|
||||||
|
premium_type: number,
|
||||||
|
customer_id: string,
|
||||||
|
subscription_id: string,
|
||||||
|
premium_expire_at: string,
|
||||||
|
created_at: string,
|
||||||
|
__v: number,
|
||||||
|
counts: { _id: string, project_id: string, events: number, visits: number, sessions: number, updated_at?: string }
|
||||||
|
}[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async event => {
|
export default defineEventHandler(async event => {
|
||||||
@@ -24,40 +27,53 @@ export default defineEventHandler(async event => {
|
|||||||
if (!userData?.logged) return;
|
if (!userData?.logged) return;
|
||||||
if (!userData.user.roles.includes('ADMIN')) return;
|
if (!userData.user.roles.includes('ADMIN')) return;
|
||||||
|
|
||||||
const data: AdminProjectsList[] = await ProjectModel.aggregate([
|
const data: AdminProjectsList[] = await UserModel.aggregate([
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: "users",
|
from: "projects",
|
||||||
localField: "owner",
|
localField: "_id",
|
||||||
foreignField: "_id",
|
foreignField: "owner",
|
||||||
as: "user"
|
as: "projects"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unwind: {
|
||||||
|
path: "$projects",
|
||||||
|
preserveNullAndEmptyArrays: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: "project_counts",
|
from: "project_counts",
|
||||||
localField: "_id",
|
localField: "projects._id",
|
||||||
foreignField: "project_id",
|
foreignField: "project_id",
|
||||||
as: "counts"
|
as: "projects.counts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$project: {
|
$addFields: {
|
||||||
project_name: "$name",
|
"projects.counts": {
|
||||||
premium: 1,
|
$arrayElemAt: ["$projects.counts", 0]
|
||||||
premium_type: 1,
|
}
|
||||||
created_at: 1,
|
}
|
||||||
user: {
|
|
||||||
$first: "$user"
|
|
||||||
},
|
},
|
||||||
total_visits: {
|
{
|
||||||
$arrayElemAt: ["$counts.visits", 0]
|
$group: {
|
||||||
|
_id: "$_id",
|
||||||
|
name: {
|
||||||
|
$first: "$name"
|
||||||
},
|
},
|
||||||
total_events: {
|
given_name: {
|
||||||
$arrayElemAt: ["$counts.events", 0]
|
$first: "$given_name"
|
||||||
},
|
},
|
||||||
total_sessions: {
|
created_at: {
|
||||||
$arrayElemAt: ["$counts.sessions", 0]
|
$first: "$created_at"
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
$first: "$email"
|
||||||
|
},
|
||||||
|
projects: {
|
||||||
|
$push: "$projects"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user