@@ -1,6 +1,17 @@
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type { HqPermissionKey } from '@dukang/shared-types';
|
||||
import type {
|
||||
DashboardAnalytics,
|
||||
DashboardGranularity,
|
||||
HqPermissionKey,
|
||||
} from '@dukang/shared-types';
|
||||
import {
|
||||
addShanghaiDays,
|
||||
defaultShanghaiRangeYmds,
|
||||
eachShanghaiBuckets,
|
||||
parseShanghaiYmd,
|
||||
shanghaiYmd,
|
||||
} from '@dukang/domain';
|
||||
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||
import {
|
||||
HqPermissionsResolver,
|
||||
@@ -9,43 +20,8 @@ import {
|
||||
type HqCityScope,
|
||||
} from '../../common/guards/hq-permission.guard';
|
||||
import type { AdminDashboardAnalyticsQueryDto } from './dto/admin-query.dto';
|
||||
|
||||
function startOfDay(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
function endOfDay(d: Date) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59, 999);
|
||||
}
|
||||
|
||||
function parseYmd(s: string): Date | null {
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return null;
|
||||
const d = new Date(`${s}T00:00:00`);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
function formatYmd(d: Date): string {
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function eachDate(from: Date, to: Date): string[] {
|
||||
const out: string[] = [];
|
||||
const cur = startOfDay(from);
|
||||
const end = startOfDay(to);
|
||||
while (cur <= end) {
|
||||
out.push(formatYmd(cur));
|
||||
cur.setDate(cur.getDate() + 1);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function num(v: Prisma.Decimal | number | string | null | undefined): number {
|
||||
if (v == null) return 0;
|
||||
return typeof v === 'number' ? v : Number(v);
|
||||
}
|
||||
import { type CityFilter } from './admin-dashboard-analytics';
|
||||
import { loadDashboardLineCharts } from './admin-dashboard-lines';
|
||||
|
||||
function isWithdrawOverdue(appliedAt: Date, now = new Date()): boolean {
|
||||
const day = appliedAt.getDay();
|
||||
@@ -296,17 +272,20 @@ export class AdminDashboardService {
|
||||
};
|
||||
}
|
||||
|
||||
async getAnalytics(actorId: bigint, query: AdminDashboardAnalyticsQueryDto) {
|
||||
const today = startOfDay(new Date());
|
||||
const defaultFrom = new Date(today);
|
||||
defaultFrom.setDate(defaultFrom.getDate() - 29);
|
||||
|
||||
const from =
|
||||
(query.dateFrom ? parseYmd(query.dateFrom) : null) ?? defaultFrom;
|
||||
const to =
|
||||
(query.dateTo ? parseYmd(query.dateTo) : null) ?? today;
|
||||
const rangeStart = startOfDay(from <= to ? from : to);
|
||||
const rangeEnd = endOfDay(from <= to ? to : from);
|
||||
async getAnalytics(
|
||||
actorId: bigint,
|
||||
query: AdminDashboardAnalyticsQueryDto,
|
||||
): Promise<DashboardAnalytics> {
|
||||
const grain: DashboardGranularity = query.granularity ?? 'day';
|
||||
const defaults = defaultShanghaiRangeYmds(grain);
|
||||
const fromYmd = /^\d{4}-\d{2}-\d{2}$/.test(query.dateFrom ?? '')
|
||||
? query.dateFrom!
|
||||
: defaults.from;
|
||||
const toYmd = /^\d{4}-\d{2}-\d{2}$/.test(query.dateTo ?? '') ? query.dateTo! : defaults.to;
|
||||
const fromDay = parseShanghaiYmd(fromYmd <= toYmd ? fromYmd : toYmd);
|
||||
const toDay = parseShanghaiYmd(fromYmd <= toYmd ? toYmd : fromYmd);
|
||||
const rangeStart = fromDay;
|
||||
const rangeEndExclusive = addShanghaiDays(toDay, 1);
|
||||
|
||||
const [{ keys, isSuperAdmin }, scope] = await Promise.all([
|
||||
this.hqPermissions.resolveAccess(actorId),
|
||||
@@ -314,518 +293,46 @@ export class AdminDashboardService {
|
||||
]);
|
||||
const can = (k: HqPermissionKey) => isSuperAdmin || keys.includes(k);
|
||||
|
||||
let filterCityCode: string | string[] | null | undefined;
|
||||
let filterCityId: bigint | bigint[] | null | undefined;
|
||||
let city: CityFilter = { kind: 'all' };
|
||||
if (query.cityId === 'none') {
|
||||
if (scope !== null) {
|
||||
throw new ForbiddenException('无权按未选城筛选');
|
||||
}
|
||||
filterCityCode = null;
|
||||
filterCityId = null;
|
||||
if (scope !== null) throw new ForbiddenException('无权按未选城筛选');
|
||||
city = { kind: 'none' };
|
||||
} else if (query.cityId) {
|
||||
const city = await this.prisma.commonCity.findUnique({
|
||||
const row = await this.prisma.commonCity.findUnique({
|
||||
where: { id: BigInt(query.cityId) },
|
||||
select: { id: true, code: true },
|
||||
});
|
||||
if (!city) {
|
||||
throw new ForbiddenException('无权访问该城市的门店');
|
||||
}
|
||||
assertHqCityInScope(scope, city.id);
|
||||
filterCityCode = city.code;
|
||||
filterCityId = city.id;
|
||||
if (!row) throw new ForbiddenException('无权访问该城市的门店');
|
||||
assertHqCityInScope(scope, row.id);
|
||||
city = { kind: 'ids', ids: [row.id], codes: [row.code] };
|
||||
} else if (scope !== null) {
|
||||
if (!scope.length) {
|
||||
filterCityId = [];
|
||||
filterCityCode = [];
|
||||
city = { kind: 'empty' };
|
||||
} else {
|
||||
const scopedCities = await this.prisma.commonCity.findMany({
|
||||
where: { id: { in: scope } },
|
||||
select: { id: true, code: true },
|
||||
});
|
||||
filterCityId = scope;
|
||||
filterCityCode = scopedCities.map((c) => c.code);
|
||||
city = { kind: 'ids', ids: scope, codes: scopedCities.map((c) => c.code) };
|
||||
}
|
||||
}
|
||||
|
||||
const filterPromoNone = query.promoCodeId === 'none';
|
||||
const filterPromoId =
|
||||
can('promo_codes') && query.promoCodeId && query.promoCodeId !== 'none'
|
||||
? BigInt(query.promoCodeId)
|
||||
: undefined;
|
||||
const filterPartnerId =
|
||||
can('partners') && query.partnerAccountId
|
||||
? BigInt(query.partnerAccountId)
|
||||
: undefined;
|
||||
|
||||
if (filterPartnerId !== undefined && scope !== null) {
|
||||
const partner = await this.prisma.partnerAccount.findUnique({
|
||||
where: { id: filterPartnerId },
|
||||
select: { cityId: true },
|
||||
});
|
||||
if (partner?.cityId) {
|
||||
assertHqCityInScope(scope, partner.cityId);
|
||||
} else if (scope !== null) {
|
||||
throw new ForbiddenException('无权访问该城市的门店');
|
||||
}
|
||||
}
|
||||
|
||||
const userWhere: Prisma.UserWhereInput = {
|
||||
status: 1,
|
||||
mergedIntoUserId: null,
|
||||
createdAt: { gte: rangeStart, lte: rangeEnd },
|
||||
};
|
||||
if (filterCityCode === null) {
|
||||
userWhere.OR = [
|
||||
{ cityPreference: null },
|
||||
{ cityPreference: { selectedCityCode: null } },
|
||||
];
|
||||
} else if (Array.isArray(filterCityCode)) {
|
||||
userWhere.cityPreference = {
|
||||
selectedCityCode: { in: filterCityCode.length ? filterCityCode : [''] },
|
||||
};
|
||||
} else if (filterCityCode) {
|
||||
userWhere.cityPreference = { selectedCityCode: filterCityCode };
|
||||
}
|
||||
if (can('promo_codes') && filterPromoNone) {
|
||||
userWhere.promoTouch = { is: null };
|
||||
} else if (filterPromoId !== undefined) {
|
||||
userWhere.promoTouch = { promoCodeId: filterPromoId };
|
||||
}
|
||||
|
||||
const orderWhere: Prisma.OrderWhereInput = {
|
||||
createdAt: { gte: rangeStart, lte: rangeEnd },
|
||||
};
|
||||
if (filterCityId === null) {
|
||||
orderWhere.id = { in: [] };
|
||||
} else if (Array.isArray(filterCityId)) {
|
||||
orderWhere.cityId = { in: filterCityId.length ? filterCityId : [BigInt(0)] };
|
||||
} else if (filterCityId !== undefined) {
|
||||
orderWhere.cityId = filterCityId;
|
||||
}
|
||||
if (can('promo_codes') && filterPromoNone) {
|
||||
orderWhere.promoCodeId = null;
|
||||
} else if (filterPromoId !== undefined) {
|
||||
orderWhere.promoCodeId = filterPromoId;
|
||||
}
|
||||
|
||||
const partnerWhere: Prisma.PartnerAccountWhereInput = {
|
||||
isPrimary: 1,
|
||||
createdAt: { gte: rangeStart, lte: rangeEnd },
|
||||
};
|
||||
if (filterCityId === null) {
|
||||
partnerWhere.cityId = null;
|
||||
} else if (Array.isArray(filterCityId)) {
|
||||
partnerWhere.cityId = { in: filterCityId.length ? filterCityId : [BigInt(0)] };
|
||||
} else if (filterCityId !== undefined) {
|
||||
partnerWhere.cityId = filterCityId;
|
||||
}
|
||||
if (filterPartnerId !== undefined) {
|
||||
partnerWhere.id = filterPartnerId;
|
||||
}
|
||||
|
||||
const storeWhere: Prisma.StoreWhereInput = {
|
||||
createdAt: { gte: rangeStart, lte: rangeEnd },
|
||||
};
|
||||
if (filterCityId === null) {
|
||||
storeWhere.id = { in: [] };
|
||||
} else if (Array.isArray(filterCityId)) {
|
||||
storeWhere.cityId = { in: filterCityId.length ? filterCityId : [BigInt(0)] };
|
||||
} else if (filterCityId !== undefined) {
|
||||
storeWhere.cityId = filterCityId;
|
||||
}
|
||||
if (filterPartnerId !== undefined) {
|
||||
storeWhere.partnerAccountId = filterPartnerId;
|
||||
}
|
||||
|
||||
const redeemWhere: Prisma.RedeemRecordWhereInput = {
|
||||
createdAt: { gte: rangeStart, lte: rangeEnd },
|
||||
};
|
||||
if (filterCityId === null) {
|
||||
redeemWhere.id = { in: [] };
|
||||
} else {
|
||||
const storeFilter: Prisma.StoreWhereInput = {};
|
||||
if (Array.isArray(filterCityId)) {
|
||||
storeFilter.cityId = { in: filterCityId.length ? filterCityId : [BigInt(0)] };
|
||||
} else if (filterCityId !== undefined) {
|
||||
storeFilter.cityId = filterCityId;
|
||||
}
|
||||
if (filterPartnerId !== undefined) storeFilter.partnerAccountId = filterPartnerId;
|
||||
if (Object.keys(storeFilter).length) {
|
||||
redeemWhere.store = storeFilter;
|
||||
}
|
||||
}
|
||||
|
||||
const skipOrders = filterCityId === null || !can('orders');
|
||||
const cityListWhere: Prisma.CommonCityWhereInput =
|
||||
scope === null ? {} : { id: { in: scope.length ? scope : [BigInt(0)] } };
|
||||
|
||||
const [users, orders, partners, stores, redeems, cities, promos, partnerNames] =
|
||||
await Promise.all([
|
||||
can('users')
|
||||
? this.prisma.user.findMany({
|
||||
where: userWhere,
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
cityPreference: { select: { selectedCityCode: true } },
|
||||
promoTouch: { select: { promoCodeId: true } },
|
||||
},
|
||||
})
|
||||
: emptyRows<
|
||||
Prisma.UserGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
createdAt: true;
|
||||
cityPreference: { select: { selectedCityCode: true } };
|
||||
promoTouch: { select: { promoCodeId: true } };
|
||||
};
|
||||
}>
|
||||
>(),
|
||||
skipOrders
|
||||
? emptyRows<
|
||||
Prisma.OrderGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
userId: true;
|
||||
createdAt: true;
|
||||
cityId: true;
|
||||
promoCodeId: true;
|
||||
payStatus: true;
|
||||
};
|
||||
}>
|
||||
>()
|
||||
: this.prisma.order.findMany({
|
||||
where: orderWhere,
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
cityId: true,
|
||||
promoCodeId: true,
|
||||
payStatus: true,
|
||||
},
|
||||
}),
|
||||
can('partners')
|
||||
? this.prisma.partnerAccount.findMany({
|
||||
where: partnerWhere,
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
cityId: true,
|
||||
companyName: true,
|
||||
name: true,
|
||||
},
|
||||
})
|
||||
: emptyRows<
|
||||
Prisma.PartnerAccountGetPayload<{
|
||||
select: { id: true; createdAt: true; cityId: true; companyName: true; name: true };
|
||||
}>
|
||||
>(),
|
||||
can('stores')
|
||||
? this.prisma.store.findMany({
|
||||
where: storeWhere,
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
cityId: true,
|
||||
partnerAccountId: true,
|
||||
},
|
||||
})
|
||||
: emptyRows<
|
||||
Prisma.StoreGetPayload<{
|
||||
select: { id: true; createdAt: true; cityId: true; partnerAccountId: true };
|
||||
}>
|
||||
>(),
|
||||
can('benefit')
|
||||
? this.prisma.redeemRecord.findMany({
|
||||
where: redeemWhere,
|
||||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
amount: true,
|
||||
settleAmount: true,
|
||||
store: { select: { cityId: true, partnerAccountId: true } },
|
||||
},
|
||||
})
|
||||
: emptyRows<
|
||||
Prisma.RedeemRecordGetPayload<{
|
||||
select: {
|
||||
id: true;
|
||||
createdAt: true;
|
||||
amount: true;
|
||||
settleAmount: true;
|
||||
store: { select: { cityId: true; partnerAccountId: true } };
|
||||
};
|
||||
}>
|
||||
>(),
|
||||
this.prisma.commonCity.findMany({
|
||||
where: cityListWhere,
|
||||
select: { id: true, code: true, name: true },
|
||||
}),
|
||||
can('promo_codes')
|
||||
? this.prisma.commonPromoCode.findMany({
|
||||
select: { id: true, code: true, name: true },
|
||||
})
|
||||
: emptyRows<Prisma.CommonPromoCodeGetPayload<{ select: { id: true; code: true; name: true } }>>(),
|
||||
can('partners') || can('stores') || can('benefit')
|
||||
? this.prisma.partnerAccount.findMany({
|
||||
where: {
|
||||
isPrimary: 1,
|
||||
...(cityIdFilter(scope) ? { cityId: cityIdFilter(scope) } : {}),
|
||||
},
|
||||
select: { id: true, companyName: true, name: true },
|
||||
})
|
||||
: emptyRows<
|
||||
Prisma.PartnerAccountGetPayload<{
|
||||
select: { id: true; companyName: true; name: true };
|
||||
}>
|
||||
>(),
|
||||
]);
|
||||
|
||||
const cityByCode = new Map(cities.map((c) => [c.code, c]));
|
||||
const cityById = new Map(cities.map((c) => [c.id.toString(), c]));
|
||||
const promoById = new Map(promos.map((p) => [p.id.toString(), p]));
|
||||
const partnerLabel = new Map(
|
||||
partnerNames.map((p) => [
|
||||
p.id.toString(),
|
||||
p.companyName || p.name || `合伙人#${p.id}`,
|
||||
]),
|
||||
);
|
||||
|
||||
const dateKeys = eachDate(rangeStart, rangeEnd);
|
||||
type DateBucket = {
|
||||
date: string;
|
||||
users: number;
|
||||
orders: number;
|
||||
partners: number;
|
||||
stores: number;
|
||||
redeems: number;
|
||||
redeemAmount: number;
|
||||
};
|
||||
const byDateMap = new Map<string, DateBucket>(
|
||||
dateKeys.map((d) => [
|
||||
d,
|
||||
{ date: d, users: 0, orders: 0, partners: 0, stores: 0, redeems: 0, redeemAmount: 0 },
|
||||
]),
|
||||
);
|
||||
|
||||
type CityBucket = {
|
||||
cityId: string;
|
||||
cityName: string;
|
||||
users: number;
|
||||
orders: number;
|
||||
partners: number;
|
||||
stores: number;
|
||||
redeems: number;
|
||||
redeemAmount: number;
|
||||
};
|
||||
const byCityMap = new Map<string, CityBucket>();
|
||||
|
||||
type PromoBucket = {
|
||||
promoCodeId: string | null;
|
||||
code: string;
|
||||
name: string;
|
||||
users: number;
|
||||
orders: number;
|
||||
};
|
||||
const byPromoMap = new Map<string, PromoBucket>();
|
||||
|
||||
type PartnerBucket = {
|
||||
partnerAccountId: string;
|
||||
companyName: string;
|
||||
stores: number;
|
||||
redeems: number;
|
||||
redeemAmount: number;
|
||||
};
|
||||
const byPartnerMap = new Map<string, PartnerBucket>();
|
||||
|
||||
const ensureCity = (key: string, cityId: string, cityName: string) => {
|
||||
let b = byCityMap.get(key);
|
||||
if (!b) {
|
||||
b = {
|
||||
cityId,
|
||||
cityName,
|
||||
users: 0,
|
||||
orders: 0,
|
||||
partners: 0,
|
||||
stores: 0,
|
||||
redeems: 0,
|
||||
redeemAmount: 0,
|
||||
};
|
||||
byCityMap.set(key, b);
|
||||
}
|
||||
return b;
|
||||
};
|
||||
|
||||
const ensurePromo = (
|
||||
key: string,
|
||||
promoCodeId: string | null,
|
||||
code: string,
|
||||
name: string,
|
||||
) => {
|
||||
let b = byPromoMap.get(key);
|
||||
if (!b) {
|
||||
b = { promoCodeId, code, name, users: 0, orders: 0 };
|
||||
byPromoMap.set(key, b);
|
||||
}
|
||||
return b;
|
||||
};
|
||||
|
||||
const ensurePartner = (key: string, companyName: string) => {
|
||||
let b = byPartnerMap.get(key);
|
||||
if (!b) {
|
||||
b = {
|
||||
partnerAccountId: key,
|
||||
companyName,
|
||||
stores: 0,
|
||||
redeems: 0,
|
||||
redeemAmount: 0,
|
||||
};
|
||||
byPartnerMap.set(key, b);
|
||||
}
|
||||
return b;
|
||||
};
|
||||
|
||||
for (const u of users) {
|
||||
const d = formatYmd(u.createdAt);
|
||||
const day = byDateMap.get(d);
|
||||
if (day) day.users += 1;
|
||||
|
||||
const code = u.cityPreference?.selectedCityCode ?? null;
|
||||
if (code && cityByCode.has(code)) {
|
||||
const city = cityByCode.get(code)!;
|
||||
ensureCity(city.id.toString(), city.id.toString(), city.name).users += 1;
|
||||
} else {
|
||||
ensureCity('none', 'none', '未选城').users += 1;
|
||||
}
|
||||
|
||||
const pid = u.promoTouch?.promoCodeId?.toString() ?? null;
|
||||
if (pid && promoById.has(pid)) {
|
||||
const p = promoById.get(pid)!;
|
||||
ensurePromo(pid, pid, p.code, p.name).users += 1;
|
||||
} else {
|
||||
ensurePromo('none', null, 'ORGANIC', '自然量').users += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const payingUserIds = new Set<string>();
|
||||
for (const o of orders) {
|
||||
const d = formatYmd(o.createdAt);
|
||||
const day = byDateMap.get(d);
|
||||
if (day) day.orders += 1;
|
||||
|
||||
const cid = o.cityId.toString();
|
||||
const city = cityById.get(cid);
|
||||
ensureCity(cid, cid, city?.name ?? `城市#${cid}`).orders += 1;
|
||||
|
||||
const pid = o.promoCodeId?.toString() ?? null;
|
||||
if (pid && promoById.has(pid)) {
|
||||
const p = promoById.get(pid)!;
|
||||
ensurePromo(pid, pid, p.code, p.name).orders += 1;
|
||||
} else {
|
||||
ensurePromo('none', null, 'NONE', '无推广码').orders += 1;
|
||||
}
|
||||
|
||||
if (o.payStatus === 'PAID') {
|
||||
payingUserIds.add(o.userId.toString());
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of partners) {
|
||||
const d = formatYmd(p.createdAt);
|
||||
const day = byDateMap.get(d);
|
||||
if (day) day.partners += 1;
|
||||
|
||||
if (p.cityId) {
|
||||
const cid = p.cityId.toString();
|
||||
const city = cityById.get(cid);
|
||||
ensureCity(cid, cid, city?.name ?? `城市#${cid}`).partners += 1;
|
||||
} else {
|
||||
ensureCity('none', 'none', '未绑定城市').partners += 1;
|
||||
}
|
||||
|
||||
const key = p.id.toString();
|
||||
ensurePartner(key, p.companyName || p.name || `合伙人#${key}`);
|
||||
}
|
||||
|
||||
for (const s of stores) {
|
||||
const d = formatYmd(s.createdAt);
|
||||
const day = byDateMap.get(d);
|
||||
if (day) day.stores += 1;
|
||||
|
||||
const cid = s.cityId.toString();
|
||||
const city = cityById.get(cid);
|
||||
ensureCity(cid, cid, city?.name ?? `城市#${cid}`).stores += 1;
|
||||
|
||||
const pid = s.partnerAccountId.toString();
|
||||
ensurePartner(pid, partnerLabel.get(pid) || `合伙人#${pid}`).stores += 1;
|
||||
}
|
||||
|
||||
let redeemAmountTotal = 0;
|
||||
for (const r of redeems) {
|
||||
const amount = num(r.amount);
|
||||
redeemAmountTotal += amount;
|
||||
|
||||
const d = formatYmd(r.createdAt);
|
||||
const day = byDateMap.get(d);
|
||||
if (day) {
|
||||
day.redeems += 1;
|
||||
day.redeemAmount += amount;
|
||||
}
|
||||
|
||||
const cid = r.store.cityId.toString();
|
||||
const city = cityById.get(cid);
|
||||
const cityBucket = ensureCity(cid, cid, city?.name ?? `城市#${cid}`);
|
||||
cityBucket.redeems += 1;
|
||||
cityBucket.redeemAmount += amount;
|
||||
|
||||
const pid = r.store.partnerAccountId.toString();
|
||||
const partnerBucket = ensurePartner(
|
||||
pid,
|
||||
partnerLabel.get(pid) || `合伙人#${pid}`,
|
||||
);
|
||||
partnerBucket.redeems += 1;
|
||||
partnerBucket.redeemAmount += amount;
|
||||
}
|
||||
|
||||
const byCity = [...byCityMap.values()].sort(
|
||||
(a, b) =>
|
||||
b.users + b.orders + b.partners + b.stores + b.redeems -
|
||||
(a.users + a.orders + a.partners + a.stores + a.redeems),
|
||||
);
|
||||
const byPromo = [...byPromoMap.values()].sort(
|
||||
(a, b) => b.users + b.orders - (a.users + a.orders),
|
||||
);
|
||||
const byPartner = [...byPartnerMap.values()].sort(
|
||||
(a, b) => b.stores + b.redeems - (a.stores + a.redeems),
|
||||
);
|
||||
const buckets = eachShanghaiBuckets(rangeStart, toDay, grain);
|
||||
const charts = await loadDashboardLineCharts({
|
||||
prisma: this.prisma,
|
||||
grain,
|
||||
city,
|
||||
rangeStart,
|
||||
rangeEndExclusive,
|
||||
periodKeys: buckets.map((b) => b.key),
|
||||
can,
|
||||
});
|
||||
|
||||
return {
|
||||
summary: {
|
||||
users: users.length,
|
||||
orders: orders.length,
|
||||
payingUsers: payingUserIds.size,
|
||||
partners: partners.length,
|
||||
stores: stores.length,
|
||||
redeems: redeems.length,
|
||||
redeemAmount: Math.round(redeemAmountTotal * 100) / 100,
|
||||
},
|
||||
byDate: dateKeys.map((d) => {
|
||||
const row = byDateMap.get(d)!;
|
||||
return {
|
||||
...row,
|
||||
redeemAmount: Math.round(row.redeemAmount * 100) / 100,
|
||||
};
|
||||
}),
|
||||
byCity: byCity.map((row) => ({
|
||||
...row,
|
||||
redeemAmount: Math.round(row.redeemAmount * 100) / 100,
|
||||
})),
|
||||
byPromo,
|
||||
byPartner: byPartner.map((row) => ({
|
||||
...row,
|
||||
redeemAmount: Math.round(row.redeemAmount * 100) / 100,
|
||||
})),
|
||||
granularity: grain,
|
||||
range: { from: shanghaiYmd(fromDay), to: shanghaiYmd(toDay) },
|
||||
periods: buckets.map((b) => ({ key: b.key, label: b.label })),
|
||||
charts,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user