save, create and view reports in dashboard
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { exampleRouter } from "@/server/api/routers/example";
|
||||
import { createTRPCRouter } from "@/server/api/trpc";
|
||||
import { chartMetaRouter } from "./routers/chartMeta";
|
||||
import { reportRouter } from "./routers/report";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -9,7 +10,8 @@ import { chartMetaRouter } from "./routers/chartMeta";
|
||||
*/
|
||||
export const appRouter = createTRPCRouter({
|
||||
example: exampleRouter,
|
||||
chartMeta: chartMetaRouter
|
||||
chartMeta: chartMetaRouter,
|
||||
report: reportRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -5,12 +5,7 @@ import { db } from "@/server/db";
|
||||
import { map, path, pipe, sort, uniq } from "ramda";
|
||||
import { toDots } from "@/utils/object";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import {
|
||||
zChartBreakdowns,
|
||||
zChartEvents,
|
||||
zChartType,
|
||||
zTimeInterval,
|
||||
} from "@/utils/validation";
|
||||
import { zChartInput } from "@/utils/validation";
|
||||
import { type IChartBreakdown, type IChartEvent } from "@/types";
|
||||
|
||||
type ResultItem = {
|
||||
@@ -21,17 +16,25 @@ type ResultItem = {
|
||||
|
||||
function propertyNameToSql(name: string) {
|
||||
if (name.includes(".")) {
|
||||
return name
|
||||
const str = name
|
||||
.split(".")
|
||||
.map((item, index) => (index === 0 ? item : `'${item}'`))
|
||||
.join("->");
|
||||
const findLastOf = "->"
|
||||
const lastArrow = str.lastIndexOf(findLastOf);
|
||||
if(lastArrow === -1) {
|
||||
return str;
|
||||
}
|
||||
const first = str.slice(0, lastArrow);
|
||||
const last = str.slice(lastArrow + findLastOf.length);
|
||||
return `${first}->>${last}`
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function getEventLegend(event: IChartEvent) {
|
||||
return `${event.name} (${event.id})`
|
||||
return `${event.name} (${event.id})`;
|
||||
}
|
||||
|
||||
function getTotalCount(arr: ResultItem[]) {
|
||||
@@ -84,7 +87,7 @@ async function getChartData({
|
||||
filters.forEach((filter) => {
|
||||
const { name, value } = filter;
|
||||
if (name.includes(".")) {
|
||||
where.push(`${propertyNameToSql(name)} = '"${value}"'`);
|
||||
where.push(`${propertyNameToSql(name)} = '${value}'`);
|
||||
} else {
|
||||
where.push(`${name} = '${value}'`);
|
||||
}
|
||||
@@ -119,9 +122,8 @@ async function getChartData({
|
||||
GROUP BY ${groupBy.join(", ")}
|
||||
ORDER BY ${orderBy.join(", ")}
|
||||
`;
|
||||
console.log(sql);
|
||||
|
||||
const result = await db.$queryRawUnsafe<ResultItem[]>(sql);
|
||||
const result = await db.$queryRawUnsafe<ResultItem[]>(sql);
|
||||
|
||||
// group by sql label
|
||||
const series = result.reduce(
|
||||
@@ -129,7 +131,7 @@ async function getChartData({
|
||||
// item.label can be null when using breakdowns on a property
|
||||
// that doesn't exist on all events
|
||||
// fallback on event legend
|
||||
const label = item.label?.trim() ?? getEventLegend(event)
|
||||
const label = item.label?.trim() ?? getEventLegend(event);
|
||||
if (label) {
|
||||
if (acc[label]) {
|
||||
acc[label]?.push(item);
|
||||
@@ -147,22 +149,19 @@ async function getChartData({
|
||||
|
||||
return Object.keys(series).map((key) => {
|
||||
const legend = breakdowns.length ? key : getEventLegend(event);
|
||||
const data = series[key] ?? []
|
||||
const data = series[key] ?? [];
|
||||
return {
|
||||
name: legend,
|
||||
totalCount: getTotalCount(data),
|
||||
data: fillEmptySpotsInTimeline(
|
||||
data,
|
||||
interval,
|
||||
startDate,
|
||||
endDate,
|
||||
).map((item) => {
|
||||
return {
|
||||
label: legend,
|
||||
count: item.count,
|
||||
date: new Date(item.date).toISOString(),
|
||||
};
|
||||
}),
|
||||
data: fillEmptySpotsInTimeline(data, interval, startDate, endDate).map(
|
||||
(item) => {
|
||||
return {
|
||||
label: legend,
|
||||
count: item.count,
|
||||
date: new Date(item.date).toISOString(),
|
||||
};
|
||||
},
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -237,16 +236,7 @@ export const chartMetaRouter = createTRPCRouter({
|
||||
}),
|
||||
|
||||
chart: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
startDate: z.date().nullish(),
|
||||
endDate: z.date().nullish(),
|
||||
chartType: zChartType,
|
||||
interval: zTimeInterval,
|
||||
events: zChartEvents,
|
||||
breakdowns: zChartBreakdowns,
|
||||
}),
|
||||
)
|
||||
.input(zChartInput)
|
||||
.query(
|
||||
async ({
|
||||
input: { chartType, events, breakdowns, interval, ...input },
|
||||
@@ -285,12 +275,17 @@ function fillEmptySpotsInTimeline(
|
||||
endDate: Date,
|
||||
) {
|
||||
const result = [];
|
||||
const currentDate = new Date(startDate);
|
||||
currentDate.setHours(2, 0, 0, 0);
|
||||
const modifiedEndDate = new Date(endDate);
|
||||
modifiedEndDate.setHours(2, 0, 0, 0);
|
||||
|
||||
while (currentDate.getTime() <= modifiedEndDate.getTime()) {
|
||||
const clonedStartDate = new Date(startDate);
|
||||
const clonedEndDate = new Date(endDate);
|
||||
if(interval === 'hour') {
|
||||
clonedStartDate.setMinutes(0, 0, 0);
|
||||
clonedEndDate.setMinutes(0, 0, 0)
|
||||
} else {
|
||||
clonedStartDate.setHours(2, 0, 0, 0);
|
||||
clonedEndDate.setHours(2, 0, 0, 0);
|
||||
}
|
||||
|
||||
while (clonedStartDate.getTime() <= clonedEndDate.getTime()) {
|
||||
const getYear = (date: Date) => date.getFullYear();
|
||||
const getMonth = (date: Date) => date.getMonth();
|
||||
const getDay = (date: Date) => date.getDate();
|
||||
@@ -302,32 +297,32 @@ function fillEmptySpotsInTimeline(
|
||||
|
||||
if (interval === "month") {
|
||||
return (
|
||||
getYear(date) === getYear(currentDate) &&
|
||||
getMonth(date) === getMonth(currentDate)
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate)
|
||||
);
|
||||
}
|
||||
if (interval === "day") {
|
||||
return (
|
||||
getYear(date) === getYear(currentDate) &&
|
||||
getMonth(date) === getMonth(currentDate) &&
|
||||
getDay(date) === getDay(currentDate)
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate) &&
|
||||
getDay(date) === getDay(clonedStartDate)
|
||||
);
|
||||
}
|
||||
if (interval === "hour") {
|
||||
return (
|
||||
getYear(date) === getYear(currentDate) &&
|
||||
getMonth(date) === getMonth(currentDate) &&
|
||||
getDay(date) === getDay(currentDate) &&
|
||||
getHour(date) === getHour(currentDate)
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate) &&
|
||||
getDay(date) === getDay(clonedStartDate) &&
|
||||
getHour(date) === getHour(clonedStartDate)
|
||||
);
|
||||
}
|
||||
if (interval === "minute") {
|
||||
return (
|
||||
getYear(date) === getYear(currentDate) &&
|
||||
getMonth(date) === getMonth(currentDate) &&
|
||||
getDay(date) === getDay(currentDate) &&
|
||||
getHour(date) === getHour(currentDate) &&
|
||||
getMinute(date) === getMinute(currentDate)
|
||||
getYear(date) === getYear(clonedStartDate) &&
|
||||
getMonth(date) === getMonth(clonedStartDate) &&
|
||||
getDay(date) === getDay(clonedStartDate) &&
|
||||
getHour(date) === getHour(clonedStartDate) &&
|
||||
getMinute(date) === getMinute(clonedStartDate)
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -336,7 +331,7 @@ function fillEmptySpotsInTimeline(
|
||||
result.push(item);
|
||||
} else {
|
||||
result.push({
|
||||
date: currentDate.toISOString(),
|
||||
date: clonedStartDate.toISOString(),
|
||||
count: 0,
|
||||
label: null,
|
||||
});
|
||||
@@ -344,19 +339,19 @@ function fillEmptySpotsInTimeline(
|
||||
|
||||
switch (interval) {
|
||||
case "day": {
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
clonedStartDate.setDate(clonedStartDate.getDate() + 1);
|
||||
break;
|
||||
}
|
||||
case "hour": {
|
||||
currentDate.setHours(currentDate.getHours() + 1);
|
||||
clonedStartDate.setHours(clonedStartDate.getHours() + 1);
|
||||
break;
|
||||
}
|
||||
case "minute": {
|
||||
currentDate.setMinutes(currentDate.getMinutes() + 1);
|
||||
clonedStartDate.setMinutes(clonedStartDate.getMinutes() + 1);
|
||||
break;
|
||||
}
|
||||
case "month": {
|
||||
currentDate.setMonth(currentDate.getMonth() + 1);
|
||||
clonedStartDate.setMonth(clonedStartDate.getMonth() + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
108
apps/web/src/server/api/routers/report.ts
Normal file
108
apps/web/src/server/api/routers/report.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import { zChartInput } from "@/utils/validation";
|
||||
import { dateDifferanceInDays, getDaysOldDate } from "@/utils/date";
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
type IChartInput,
|
||||
type IChartBreakdown,
|
||||
type IChartEvent,
|
||||
} from "@/types";
|
||||
import { type Report as DbReport } from "@prisma/client";
|
||||
|
||||
function transform(report: DbReport): IChartInput & { id: string } {
|
||||
return {
|
||||
id: report.id,
|
||||
events: report.events as IChartEvent[],
|
||||
breakdowns: report.breakdowns as IChartBreakdown[],
|
||||
startDate: getDaysOldDate(report.range),
|
||||
endDate: new Date(),
|
||||
chartType: report.chart_type,
|
||||
interval: report.interval,
|
||||
name: report.name,
|
||||
};
|
||||
}
|
||||
|
||||
export const reportRouter = createTRPCRouter({
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(({ input: { id } }) => {
|
||||
return db.report
|
||||
.findUniqueOrThrow({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
.then(transform);
|
||||
}),
|
||||
getDashboard: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
dashboardId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input: { projectId, dashboardId } }) => {
|
||||
const reports = await db.report.findMany({
|
||||
where: {
|
||||
project_id: projectId,
|
||||
dashboard_id: dashboardId,
|
||||
},
|
||||
});
|
||||
|
||||
return reports.map(transform);
|
||||
}),
|
||||
save: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
report: zChartInput,
|
||||
projectId: z.string(),
|
||||
dashboardId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(({ input: { report, projectId, dashboardId } }) => {
|
||||
return db.report.create({
|
||||
data: {
|
||||
project_id: projectId,
|
||||
dashboard_id: dashboardId,
|
||||
name: report.name,
|
||||
events: report.events,
|
||||
interval: report.interval,
|
||||
breakdowns: report.breakdowns,
|
||||
chart_type: report.chartType,
|
||||
range: dateDifferanceInDays(report.endDate, report.startDate),
|
||||
},
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
reportId: z.string(),
|
||||
report: zChartInput,
|
||||
projectId: z.string(),
|
||||
dashboardId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(({ input: { report, projectId, dashboardId, reportId } }) => {
|
||||
return db.report.update({
|
||||
where: {
|
||||
id: reportId,
|
||||
},
|
||||
data: {
|
||||
project_id: projectId,
|
||||
dashboard_id: dashboardId,
|
||||
name: report.name,
|
||||
events: report.events,
|
||||
interval: report.interval,
|
||||
breakdowns: report.breakdowns,
|
||||
chart_type: report.chartType,
|
||||
range: dateDifferanceInDays(report.endDate, report.startDate),
|
||||
},
|
||||
});
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user