mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-09 23:48:36 +01:00
update snapthots + admin panel users
This commit is contained in:
@@ -2,14 +2,34 @@
|
|||||||
|
|
||||||
import { useSelectMenuStyle } from '~/composables/ui/useSelectMenuStyle';
|
import { useSelectMenuStyle } from '~/composables/ui/useSelectMenuStyle';
|
||||||
import type { TAdminUser } from '~/server/api/admin/users';
|
import type { TAdminUser } from '~/server/api/admin/users';
|
||||||
|
import { sub, format, isSameDay, type Duration, startOfDay, endOfDay } from 'date-fns'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const filterText = ref<string>('');
|
const filterText = ref<string>('');
|
||||||
|
|
||||||
watch(filterText,()=>{
|
watch(filterText, () => {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function isRangeSelected(duration: Duration) {
|
||||||
|
return isSameDay(selected.value.start, sub(new Date(), duration)) && isSameDay(selected.value.end, new Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectRange(duration: Duration) {
|
||||||
|
selected.value = { start: sub(new Date(), duration), end: new Date() }
|
||||||
|
}
|
||||||
|
|
||||||
|
const ranges = [
|
||||||
|
{ label: 'Last 7 days', duration: { days: 7 } },
|
||||||
|
{ label: 'Last 14 days', duration: { days: 14 } },
|
||||||
|
{ label: 'Last 30 days', duration: { days: 30 } },
|
||||||
|
{ label: 'Last 3 months', duration: { months: 3 } },
|
||||||
|
{ label: 'Last 6 months', duration: { months: 6 } },
|
||||||
|
{ label: 'Last year', duration: { years: 1 } }
|
||||||
|
]
|
||||||
|
const selected = ref<{ start: Date, end: Date }>({ start: sub(new Date(), { days: 14 }), end: new Date() })
|
||||||
|
|
||||||
const filter = computed(() => {
|
const filter = computed(() => {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
$or: [
|
$or: [
|
||||||
@@ -39,7 +59,7 @@ const limitList = [
|
|||||||
const limit = ref<number>(20);
|
const limit = ref<number>(20);
|
||||||
|
|
||||||
const { data: usersInfo, pending: pendingUsers } = await useFetch<{ count: number, users: TAdminUser[] }>(
|
const { data: usersInfo, pending: pendingUsers } = await useFetch<{ count: number, users: TAdminUser[] }>(
|
||||||
() => `/api/admin/users?page=${page.value - 1}&limit=${limit.value}&sortQuery=${order.value}&filterQuery=${filter.value}`,
|
() => `/api/admin/users?page=${page.value - 1}&limit=${limit.value}&sortQuery=${order.value}&filterQuery=${filter.value}&filterFrom=${selected.value.start.toISOString()}&filterTo=${selected.value.end.toISOString()}`,
|
||||||
signHeaders()
|
signHeaders()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -51,44 +71,71 @@ const { uiMenu } = useSelectMenuStyle();
|
|||||||
<div class="mt-6 h-full">
|
<div class="mt-6 h-full">
|
||||||
|
|
||||||
|
|
||||||
<div class="flex items-center gap-10 px-10">
|
<div class="flex flex-col items-center gap-6">
|
||||||
|
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex items-center gap-10 px-10">
|
||||||
<div>Order:</div>
|
<div class="flex gap-2 items-center">
|
||||||
<USelectMenu :uiMenu="uiMenu" class="w-[12rem]" placeholder="Order" :options="ordersList"
|
<div>Order:</div>
|
||||||
value-attribute="id" option-attribute="label" v-model="order">
|
<USelectMenu :uiMenu="uiMenu" class="w-[12rem]" placeholder="Order" :options="ordersList"
|
||||||
</USelectMenu>
|
value-attribute="id" option-attribute="label" v-model="order">
|
||||||
</div>
|
</USelectMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div>Limit:</div>
|
<div>Limit:</div>
|
||||||
<USelectMenu :uiMenu="uiMenu" class="w-[12rem]" placeholder="Limit" :options="limitList"
|
<USelectMenu :uiMenu="uiMenu" class="w-[12rem]" placeholder="Limit" :options="limitList"
|
||||||
value-attribute="id" option-attribute="label" v-model="limit">
|
value-attribute="id" option-attribute="label" v-model="limit">
|
||||||
</USelectMenu>
|
</USelectMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<LyxUiInput placeholder="Search user" class="px-2 py-1" v-model="filterText"></LyxUiInput>
|
<LyxUiInput placeholder="Search user" class="px-2 py-1" v-model="filterText"></LyxUiInput>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex gap-2 items-center">
|
|
||||||
<div>Page {{ page }} </div>
|
|
||||||
<div>
|
|
||||||
{{ Math.min(limit, usersInfo?.count || 0) }}
|
|
||||||
of
|
|
||||||
{{ usersInfo?.count || 0 }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="flex items-centet gap-10">
|
||||||
<UPagination v-model="page" :page-count="limit" :total="usersInfo?.count || 0" />
|
<div class="flex gap-2 items-center">
|
||||||
|
<div>Page {{ page }} </div>
|
||||||
|
<div>
|
||||||
|
{{ Math.min(limit, usersInfo?.count || 0) }}
|
||||||
|
of
|
||||||
|
{{ usersInfo?.count || 0 }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<UPagination v-model="page" :page-count="limit" :total="usersInfo?.count || 0" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UPopover class="w-[20rem]" :popper="{ placement: 'bottom' }">
|
||||||
|
<UButton class="w-full" color="primary" variant="solid">
|
||||||
|
<div class="flex items-center justify-center w-full gap-2">
|
||||||
|
<i class="i-heroicons-calendar-days-20-solid"></i>
|
||||||
|
{{ selected.start.toLocaleDateString() }} - {{ selected.end.toLocaleDateString() }}
|
||||||
|
</div>
|
||||||
|
</UButton>
|
||||||
|
<template #panel="{ close }">
|
||||||
|
<div class="flex items-center sm:divide-x divide-gray-200 dark:divide-gray-800">
|
||||||
|
<div class="hidden sm:flex flex-col py-4">
|
||||||
|
<UButton v-for="(range, index) in ranges" :key="index" :label="range.label" color="gray"
|
||||||
|
variant="ghost" class="rounded-none px-6"
|
||||||
|
:class="[isRangeSelected(range.duration) ? 'bg-gray-100 dark:bg-gray-800' : 'hover:bg-gray-50 dark:hover:bg-gray-800/50']"
|
||||||
|
truncate @click="selectRange(range.duration)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DatePicker v-model="selected" @close="close" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="cursor-default flex justify-center flex-wrap gap-6 mb-[4rem] mt-4 overflow-auto h-full pt-6 pb-[8rem]">
|
class="cursor-default flex justify-center flex-wrap gap-6 mb-[4rem] mt-4 overflow-auto h-full pt-6 pb-[8rem]">
|
||||||
|
|
||||||
|
|||||||
@@ -64,17 +64,17 @@ async function declineInvite(project_id: string) {
|
|||||||
}">
|
}">
|
||||||
<div class="h-full flex flex-col gap-8 p-6">
|
<div class="h-full flex flex-col gap-8 p-6">
|
||||||
|
|
||||||
<div class="flex flex-col gap-2" v-for="invite of invites">
|
<div class="flex flex-col gap-6" v-for="invite of invites">
|
||||||
|
|
||||||
<div class="dark:text-lyx-text text-lyx-lightmode-text">
|
<div class="dark:text-lyx-text text-lyx-lightmode-text">
|
||||||
You are invited to join
|
You are invited to join
|
||||||
<span class="font-semibold">{{ invite.project_name }}</span>.
|
<span class="font-semibold">{{ invite.project_name }}</span>.
|
||||||
Do you accept this invitation?
|
Do you accept?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4 w-full justify-end">
|
||||||
|
<LyxUiButton @click="declineInvite(invite.project_id)" type="secondary"> Decline </LyxUiButton>
|
||||||
<LyxUiButton @click="acceptInvite(invite.project_id)" type="primary"> Accept </LyxUiButton>
|
<LyxUiButton @click="acceptInvite(invite.project_id)" type="primary"> Accept </LyxUiButton>
|
||||||
<LyxUiButton @click="declineInvite(invite.project_id)" type="danger"> Decline </LyxUiButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,9 +46,17 @@ async function save(member_id: string) {
|
|||||||
}">
|
}">
|
||||||
<div class="p-8">
|
<div class="p-8">
|
||||||
<div v-if="member" class="manage flex flex-col gap-4">
|
<div v-if="member" class="manage flex flex-col gap-4">
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="poppins text-[1.1rem]"> Manage permissions </div>
|
||||||
|
<div class="poppins text-[.9rem] dark:text-lyx-text-dark"> Choose what this member can do on this project. </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<LyxUiSeparator></LyxUiSeparator>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1"> Allowed domains </div>
|
<div class="mb-1"> Select what domain is allowed to see: </div>
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
<USelectMenu v-model="member.permission.domains" :options="domainList" multiple
|
<USelectMenu v-model="member.permission.domains" :options="domainList" multiple
|
||||||
value-attribute="_id">
|
value-attribute="_id">
|
||||||
@@ -89,7 +97,7 @@ async function save(member_id: string) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<UCheckbox v-model="member.permission.ai"></UCheckbox>
|
<UCheckbox v-model="member.permission.ai"></UCheckbox>
|
||||||
<div> Allow AI page </div>
|
<div> Allow to use AI data analyst </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,10 +106,10 @@ async function save(member_id: string) {
|
|||||||
|
|
||||||
|
|
||||||
<div class="flex gap-2 justify-end mt-8">
|
<div class="flex gap-2 justify-end mt-8">
|
||||||
<LyxUiButton v-if="member?.permission" @click="save(member._id.toString())" type="primary">
|
<LyxUiButton class="!w-[6rem] text-center" type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
||||||
|
<LyxUiButton class="!w-[6rem] text-center" v-if="member?.permission" @click="save(member._id.toString())" type="primary">
|
||||||
Save
|
Save
|
||||||
</LyxUiButton>
|
</LyxUiButton>
|
||||||
<LyxUiButton type="secondary" @click="emit('cancel')"> Cancel </LyxUiButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
name: 'Last 30 days',
|
name: 'Last 30 days',
|
||||||
from: fns.startOfDay(fns.subDays(Date.now(), 30)),
|
from: fns.startOfDay(fns.subDays(Date.now(), 30)),
|
||||||
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
||||||
color: '#BC5090',
|
color: '#606c38',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
name: 'Last 60 days',
|
name: 'Last 60 days',
|
||||||
from: fns.startOfDay(fns.subDays(Date.now(), 60)),
|
from: fns.startOfDay(fns.subDays(Date.now(), 60)),
|
||||||
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
||||||
color: '#BC5090',
|
color: '#bc6c25',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,15 +108,18 @@ export function getDefaultSnapshots(project_id: TProjectSnapshot['project_id'],
|
|||||||
name: 'Last 90 days',
|
name: 'Last 90 days',
|
||||||
from: fns.startOfDay(fns.subDays(Date.now(), 90)),
|
from: fns.startOfDay(fns.subDays(Date.now(), 90)),
|
||||||
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
to: fns.endOfDay(fns.subDays(Date.now(), 0)),
|
||||||
color: '#BC5090',
|
color: '#fefae0',
|
||||||
default: true
|
default: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const snapshotList = [
|
const snapshotList = [
|
||||||
lastDay, today, lastMonth, currentMonth,
|
allTime,
|
||||||
lastWeek, currentWeek, allTime,
|
lastDay, today,
|
||||||
last30Days, last60Days, last90Days
|
lastWeek, currentWeek,
|
||||||
|
lastMonth, currentMonth,
|
||||||
|
last30Days,
|
||||||
|
last60Days, last90Days,
|
||||||
]
|
]
|
||||||
|
|
||||||
return snapshotList;
|
return snapshotList;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const remoteSnapshots = useFetch<TProjectSnapshot[]>('/api/project/snapshots', {
|
|||||||
|
|
||||||
watch(project, async () => {
|
watch(project, async () => {
|
||||||
await remoteSnapshots.refresh();
|
await remoteSnapshots.refresh();
|
||||||
snapshot.value = isLiveDemo.value ? snapshots.value[3] : snapshots.value[3];
|
snapshot.value = isLiveDemo.value ? snapshots.value[7] : snapshots.value[7];
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshots = computed<GenericSnapshot[]>(() => {
|
const snapshots = computed<GenericSnapshot[]>(() => {
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ async function leaveProject() {
|
|||||||
alert(ex.message);
|
alert(ex.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -129,8 +128,8 @@ async function leaveProject() {
|
|||||||
</LyxUiInput>
|
</LyxUiInput>
|
||||||
<LyxUiButton @click="addMember" type="secondary"> Add </LyxUiButton>
|
<LyxUiButton @click="addMember" type="secondary"> Add </LyxUiButton>
|
||||||
</div>
|
</div>
|
||||||
<div class="poppins text-[.8rem] mt-2 text-lyx-text-darker">
|
<div class="poppins text-[.8rem] mt-2 dark:text-lyx-text-dark">
|
||||||
User should have been registered to Litlyx
|
We will send an invitation email to the user you wish to add to this project.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -150,7 +149,7 @@ async function leaveProject() {
|
|||||||
|
|
||||||
<template #pending-data="e">
|
<template #pending-data="e">
|
||||||
<div class="text-lyx-lightmode-text dark:text-lyx-text">
|
<div class="text-lyx-lightmode-text dark:text-lyx-text">
|
||||||
{{ e.row.pending ? 'Pending' : 'Ok' }}
|
{{ e.row.pending ? 'Pending' : 'Accepted' }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -166,6 +165,10 @@ async function leaveProject() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-2 flex-wrap">
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<UBadge variant="outline" size="sm" color="yellow"
|
||||||
|
v-if="!e.row.permission.webAnalytics && !e.row.permission.events && !e.row.permission.ai && e.row.permission.domains.length == 0">
|
||||||
|
No permission given
|
||||||
|
</UBadge>
|
||||||
<UBadge variant="outline" size="sm" v-if="e.row.permission.webAnalytics"
|
<UBadge variant="outline" size="sm" v-if="e.row.permission.webAnalytics"
|
||||||
label="Analytics"> </UBadge>
|
label="Analytics"> </UBadge>
|
||||||
<UBadge variant="outline" size="sm" v-if="e.row.permission.events" label="Events">
|
<UBadge variant="outline" size="sm" v-if="e.row.permission.events" label="Events">
|
||||||
@@ -188,7 +191,7 @@ async function leaveProject() {
|
|||||||
<template #action-data="e" v-if="!isGuest">
|
<template #action-data="e" v-if="!isGuest">
|
||||||
<div @click="kickMember(e.row.email)" v-if="e.row.role != 'OWNER'"
|
<div @click="kickMember(e.row.email)" v-if="e.row.role != 'OWNER'"
|
||||||
class="text-red-500 hover:bg-black/20 cursor-pointer outline outline-[1px] outline-red-500 px-3 py-1 rounded-lg text-center">
|
class="text-red-500 hover:bg-black/20 cursor-pointer outline outline-[1px] outline-red-500 px-3 py-1 rounded-lg text-center">
|
||||||
Kick
|
Remove
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,27 @@ 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 { page, limit, sortQuery, filterQuery } = getQuery(event);
|
const { page, limit, sortQuery, filterQuery, filterFrom, filterTo } = getQuery(event);
|
||||||
|
|
||||||
const pageNumber = parseInt(page as string);
|
const pageNumber = parseInt(page as string);
|
||||||
const limitNumber = parseInt(limit as string);
|
const limitNumber = parseInt(limit as string);
|
||||||
|
|
||||||
const count = await UserModel.countDocuments(JSON.parse(filterQuery as string));
|
const matchQuery = {
|
||||||
|
...JSON.parse(filterQuery as string),
|
||||||
|
created_at: {
|
||||||
|
$gte: new Date(filterFrom as string),
|
||||||
|
$lte: new Date(filterTo as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const count = await UserModel.countDocuments(matchQuery);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const users = await UserModel.aggregate([
|
const users = await UserModel.aggregate([
|
||||||
{
|
{
|
||||||
$match: JSON.parse(filterQuery as string)
|
$match: matchQuery
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
|
|||||||
Reference in New Issue
Block a user