diff --git a/apps/web/src/components/report/ReportDateRange.tsx b/apps/web/src/components/report/ReportDateRange.tsx index 2904f803..4a297108 100644 --- a/apps/web/src/components/report/ReportDateRange.tsx +++ b/apps/web/src/components/report/ReportDateRange.tsx @@ -4,27 +4,29 @@ import { changeDateRanges, changeInterval } from "./reportSlice"; import { Combobox } from "../ui/combobox"; import { type IInterval } from "@/types"; import { timeRanges } from "@/utils/constants"; -import { entries } from "@/utils/object"; export function ReportDateRange() { const dispatch = useDispatch(); + const range = useSelector((state) => state.report.range); const interval = useSelector((state) => state.report.interval); const chartType = useSelector((state) => state.report.chartType); return ( <> - {entries(timeRanges).map(([range, title]) => ( - { - dispatch(changeDateRanges(range)); - }} - > - {title} - - ))} + {timeRanges.map(item => { + return ( + { + dispatch(changeDateRanges(item.range)); + }} + > + {item.title} + + ) + })} {chartType === "linear" && (
diff --git a/apps/web/src/components/report/chart/index.tsx b/apps/web/src/components/report/chart/index.tsx index a8233505..edaebd63 100644 --- a/apps/web/src/components/report/chart/index.tsx +++ b/apps/web/src/components/report/chart/index.tsx @@ -8,23 +8,23 @@ type ReportLineChartProps = IChartInput export const Chart = withChartProivder(({ interval, - startDate, - endDate, events, breakdowns, chartType, name, + range, }: ReportLineChartProps) => { const hasEmptyFilters = events.some((event) => event.filters.some((filter) => filter.value.length === 0)); const chart = api.chart.chart.useQuery( { interval, chartType, - startDate, - endDate, events, breakdowns, name, + range, + startDate: null, + endDate: null, }, { keepPreviousData: true, diff --git a/apps/web/src/components/report/reportSlice.ts b/apps/web/src/components/report/reportSlice.ts index 3948bdf1..668949af 100644 --- a/apps/web/src/components/report/reportSlice.ts +++ b/apps/web/src/components/report/reportSlice.ts @@ -4,22 +4,26 @@ import { type IChartEvent, type IInterval, type IChartType, + type IChartRange, } from "@/types"; import { alphabetIds } from "@/utils/constants"; -import { getDaysOldDate } from "@/utils/date"; import { type PayloadAction, createSlice } from "@reduxjs/toolkit"; -type InitialState = IChartInput; +type InitialState = IChartInput & { + startDate: string | null; + endDate: string | null; +} // First approach: define the initial state using that type const initialState: InitialState = { name: "screen_view", chartType: "linear", - startDate: getDaysOldDate(7).toISOString(), - endDate: new Date().toISOString(), interval: "day", breakdowns: [], events: [], + range: 30, + startDate: null, + endDate: null, }; export const reportSlice = createSlice({ @@ -30,7 +34,11 @@ export const reportSlice = createSlice({ return initialState }, setReport(state, action: PayloadAction) { - return action.payload + return { + ...action.payload, + startDate: null, + endDate: null, + } }, // Events addEvent: (state, action: PayloadAction>) => { @@ -107,21 +115,9 @@ export const reportSlice = createSlice({ state.endDate = action.payload; }, - changeDateRanges: (state, action: PayloadAction) => { - if(action.payload === 'today') { - const startDate = new Date() - startDate.setHours(0,0,0,0) - - state.startDate = startDate.toISOString(); - state.endDate = new Date().toISOString(); - state.interval = 'hour' - return state - } - - state.startDate = getDaysOldDate(action.payload).toISOString(); - state.endDate = new Date().toISOString() - - if (action.payload === 1) { + changeDateRanges: (state, action: PayloadAction) => { + state.range = action.payload + if (action.payload === 0 || action.payload === 1) { state.interval = "hour"; } else if (action.payload <= 30) { state.interval = "day"; diff --git a/apps/web/src/modals/AddProject.tsx b/apps/web/src/modals/AddProject.tsx index 2543b48b..a660206a 100644 --- a/apps/web/src/modals/AddProject.tsx +++ b/apps/web/src/modals/AddProject.tsx @@ -9,6 +9,7 @@ import { popModal } from "."; import { toast } from "@/components/ui/use-toast"; import { InputWithLabel } from "@/components/forms/InputWithLabel"; import { useRefetchActive } from "@/hooks/useRefetchActive"; +import { useOrganizationParams } from "@/hooks/useOrganizationParams"; const validator = z.object({ name: z.string().min(1), @@ -17,6 +18,7 @@ const validator = z.object({ type IForm = z.infer; export default function AddProject() { + const params = useOrganizationParams() const refetch = useRefetchActive() const mutation = api.project.create.useMutation({ onError: handleError, @@ -41,7 +43,10 @@ export default function AddProject() {
{ - mutation.mutate(values); + mutation.mutate({ + ...values, + organizationSlug: params.organization, + }); })} > diff --git a/apps/web/src/pages/[organization]/[project]/index.tsx b/apps/web/src/pages/[organization]/[project]/index.tsx index bcf4e32a..f16c299d 100644 --- a/apps/web/src/pages/[organization]/[project]/index.tsx +++ b/apps/web/src/pages/[organization]/[project]/index.tsx @@ -12,7 +12,6 @@ export const getServerSideProps = createServerSideProps() export default function Home() { const params = useOrganizationParams(); const query = api.dashboard.list.useQuery({ - organizationSlug: params.organization, projectSlug: params.project, }, { enabled: Boolean(params.organization && params.project), diff --git a/apps/web/src/server/api/routers/chart.ts b/apps/web/src/server/api/routers/chart.ts index 8d6638bc..3b553f73 100644 --- a/apps/web/src/server/api/routers/chart.ts +++ b/apps/web/src/server/api/routers/chart.ts @@ -3,8 +3,13 @@ import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; import { db } from "@/server/db"; import { last, pipe, sort, uniq } from "ramda"; import { toDots } from "@/utils/object"; -import { zChartInput } from "@/utils/validation"; -import { type IChartInput, type IChartEvent } from "@/types"; +import { zChartInputWithDates } from "@/utils/validation"; +import { + type IChartInputWithDates, + type IChartEvent, + type IChartRange, +} from "@/types"; +import { getDaysOldDate } from "@/utils/date"; export const config = { api: { @@ -89,18 +94,14 @@ export const chartRouter = createTRPCRouter({ }), chart: protectedProcedure - .input(zChartInput) + .input(zChartInputWithDates) .query(async ({ input: { events, ...input } }) => { - const startDate = input.startDate ?? new Date(); - const endDate = input.endDate ?? new Date(); const series: Awaited> = []; for (const event of events) { series.push( ...(await getChartData({ ...input, event, - startDate, - endDate, })), ); } @@ -121,16 +122,21 @@ export const chartRouter = createTRPCRouter({ }; return { - events: Object.entries(series.reduce((acc, item) => { - if(acc[item.event.id]) { - acc[item.event.id] += item.totalCount; - } else { - acc[item.event.id] = item.totalCount; - } - return acc - }, {} as Record)).map(([id, count]) => ({ - count, - ...events.find((event) => event.id === id)!, + events: Object.entries( + series.reduce( + (acc, item) => { + if (acc[item.event.id]) { + acc[item.event.id] += item.totalCount; + } else { + acc[item.event.id] = item.totalCount; + } + return acc; + }, + {} as Record<(typeof series)[number]["event"]["id"], number>, + ), + ).map(([id, count]) => ({ + count, + ...events.find((event) => event.id === id)!, })), series: sorted.map((item) => ({ ...item, @@ -184,16 +190,45 @@ function getTotalCount(arr: ResultItem[]) { return arr.reduce((acc, item) => acc + item.count, 0); } +function getDatesFromRange(range: IChartRange) { + if (range === 0) { + const startDate = new Date(); + const endDate = new Date().toISOString(); + startDate.setHours(0, 0, 0, 0); + + return { + startDate: startDate.toISOString(), + endDate: endDate, + }; + } + + const startDate = getDaysOldDate(range).toISOString(); + const endDate = new Date().toISOString(); + return { + startDate, + endDate, + }; +} + async function getChartData({ chartType, event, breakdowns, interval, - startDate, - endDate, + range, + startDate: _startDate, + endDate: _endDate, }: { event: IChartEvent; -} & Omit) { +} & Omit) { + const { startDate, endDate } = + _startDate && _endDate + ? { + startDate: _startDate, + endDate: _endDate, + } + : getDatesFromRange(range); + const select = []; const where = []; const groupBy = []; @@ -362,6 +397,8 @@ function fillEmptySpotsInTimeline( const result = []; const clonedStartDate = new Date(startDate); const clonedEndDate = new Date(endDate); + const today = new Date(); + if (interval === "hour") { clonedStartDate.setMinutes(0, 0, 0); clonedEndDate.setMinutes(0, 0, 0); @@ -370,7 +407,16 @@ function fillEmptySpotsInTimeline( clonedEndDate.setHours(2, 0, 0, 0); } - while (clonedStartDate.getTime() <= clonedEndDate.getTime()) { + // Force if interval is month and the start date is the same month as today + const shouldForce = () => + interval === "month" && + clonedStartDate.getFullYear() === today.getFullYear() && + clonedStartDate.getMonth() === today.getMonth(); + + while ( + shouldForce() || + clonedStartDate.getTime() <= clonedEndDate.getTime() + ) { const getYear = (date: Date) => date.getFullYear(); const getMonth = (date: Date) => date.getMonth(); const getDay = (date: Date) => date.getDate(); diff --git a/apps/web/src/server/api/routers/report.ts b/apps/web/src/server/api/routers/report.ts index e788af78..46ddaf31 100644 --- a/apps/web/src/server/api/routers/report.ts +++ b/apps/web/src/server/api/routers/report.ts @@ -2,13 +2,13 @@ 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, type IChartEventFilter, + type IChartRange, } from "@/types"; import { type Report as DbReport } from "@prisma/client"; import { getProjectBySlug } from "@/server/services/project.service"; @@ -39,11 +39,10 @@ function transformReport(report: DbReport): IChartInput & { id: string } { id: report.id, events: (report.events as IChartEvent[]).map(transformEvent), breakdowns: report.breakdowns as IChartBreakdown[], - startDate: getDaysOldDate(report.range).toISOString(), - endDate: new Date().toISOString(), chartType: report.chart_type, interval: report.interval, name: report.name || 'Untitled', + range: report.range as IChartRange ?? 30, }; } @@ -103,7 +102,7 @@ export const reportRouter = createTRPCRouter({ interval: report.interval, breakdowns: report.breakdowns, chart_type: report.chartType, - range: dateDifferanceInDays(new Date(report.endDate), new Date(report.startDate)), + range: report.range, }, }); @@ -130,7 +129,7 @@ export const reportRouter = createTRPCRouter({ interval: report.interval, breakdowns: report.breakdowns, chart_type: report.chartType, - range: dateDifferanceInDays(new Date(report.endDate), new Date(report.startDate)), + range: report.range, }, }); }), diff --git a/apps/web/src/types/index.ts b/apps/web/src/types/index.ts index 31d15340..c792c712 100644 --- a/apps/web/src/types/index.ts +++ b/apps/web/src/types/index.ts @@ -1,5 +1,6 @@ import { type RouterOutputs } from "@/utils/api"; -import { type zTimeInterval, type zChartBreakdown, type zChartEvent, type zChartInput, type zChartType } from "@/utils/validation"; +import { type timeRanges } from "@/utils/constants"; +import { type zTimeInterval, type zChartBreakdown, type zChartEvent, type zChartInput, type zChartType, type zChartInputWithDates } from "@/utils/validation"; import { type Client, type Project } from "@prisma/client"; import { type TooltipProps } from "recharts"; import { type z } from "zod"; @@ -7,6 +8,7 @@ import { type z } from "zod"; export type HtmlProps = React.DetailedHTMLProps, T>; export type IChartInput = z.infer +export type IChartInputWithDates = z.infer export type IChartEvent = z.infer export type IChartEventFilter = IChartEvent['filters'][number] export type IChartEventFilterValue = IChartEvent['filters'][number]['value'][number] @@ -14,7 +16,7 @@ export type IChartBreakdown = z.infer export type IInterval = z.infer export type IChartType = z.infer export type IChartData = RouterOutputs["chart"]["chart"]; - +export type IChartRange = typeof timeRanges[number]['range']; export type IToolTipProps = Omit, 'payload'> & { payload?: Array } diff --git a/apps/web/src/utils/constants.ts b/apps/web/src/utils/constants.ts index 26af4d75..b411aa86 100644 --- a/apps/web/src/utils/constants.ts +++ b/apps/web/src/utils/constants.ts @@ -19,15 +19,26 @@ export const intervals = { month: "Month", }; -export const alphabetIds = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] as const; +export const alphabetIds = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", +] as const; -export const timeRanges = { - 'today': 'Today', - 1: '24 hours', - 7: '7 days', - 14: '14 days', - 30: '30 days', - 90: '3 months', - 180: '6 months', - 365: '1 year', -} \ No newline at end of file +export const timeRanges = [ + { range: 0, title: "Today" }, + { range: 1, title: "24 hours" }, + { range: 7, title: "7 days" }, + { range: 14, title: "14 days" }, + { range: 30, title: "30 days" }, + { range: 90, title: "3 months" }, + { range: 180, title: "6 months" }, + { range: 365, title: "1 year" }, +] as const diff --git a/apps/web/src/utils/object.ts b/apps/web/src/utils/object.ts index b5af9648..37aabe34 100644 --- a/apps/web/src/utils/object.ts +++ b/apps/web/src/utils/object.ts @@ -15,10 +15,4 @@ export function toDots( [`${path}${key}`]: value, }; }, {}); -} - -export function entries( - obj: Record, -): [K, V][] { - return Object.entries(obj) as [K, V][]; -} +} \ No newline at end of file diff --git a/apps/web/src/utils/validation.ts b/apps/web/src/utils/validation.ts index 401b0984..55d9c1de 100644 --- a/apps/web/src/utils/validation.ts +++ b/apps/web/src/utils/validation.ts @@ -1,9 +1,9 @@ import { z } from "zod"; import { operators, chartTypes, intervals } from "./constants"; -function objectToZodEnums ( obj: Record ): [ K, ...K[] ] { - const [ firstKey, ...otherKeys ] = Object.keys( obj ) as K[] - return [ firstKey!, ...otherKeys ] +function objectToZodEnums(obj: Record): [K, ...K[]] { + const [firstKey, ...otherKeys] = Object.keys(obj) as K[]; + return [firstKey!, ...otherKeys]; } export const zChartEvent = z.object({ @@ -15,13 +15,7 @@ export const zChartEvent = z.object({ id: z.string(), name: z.string(), operator: z.enum(objectToZodEnums(operators)), - value: z.array( - z - .string() - .or(z.number()) - .or(z.boolean()) - .or(z.null()) - ), + value: z.array(z.string().or(z.number()).or(z.boolean()).or(z.null())), }), ), }); @@ -39,10 +33,22 @@ export const zTimeInterval = z.enum(objectToZodEnums(intervals)); export const zChartInput = z.object({ name: z.string(), - startDate: z.string(), - endDate: z.string(), chartType: zChartType, interval: zTimeInterval, events: zChartEvents, breakdowns: zChartBreakdowns, + range: z + .literal(0) + .or(z.literal(1)) + .or(z.literal(7)) + .or(z.literal(14)) + .or(z.literal(30)) + .or(z.literal(90)) + .or(z.literal(180)) + .or(z.literal(365)), +}); + +export const zChartInputWithDates = zChartInput.extend({ + startDate: z.string().nullish(), + endDate: z.string().nullable(), });