feat: report editor

commit bfcf271a64c33a60f61f511cec2198d9c8a9c51a
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Wed Nov 26 12:32:40 2025 +0100

    wip

commit 8cd3b89fa3
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Tue Nov 25 22:33:58 2025 +0100

    funnel

commit 95af86dc44
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Tue Nov 25 22:23:25 2025 +0100

    wip

commit 727a218e6b
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Tue Nov 25 10:18:26 2025 +0100

    conversion wip

commit 958ba535d6
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Tue Nov 25 10:18:20 2025 +0100

    wip

commit 3bbeb927cc
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Tue Nov 25 09:18:48 2025 +0100

    wip

commit d99335e2f4
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Mon Nov 24 18:08:10 2025 +0100

    wip

commit 1fa61b1ae9
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Mon Nov 24 15:50:28 2025 +0100

    ts

commit 548747d826
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Mon Nov 24 13:17:01 2025 +0100

    fix typecheck events -> series

commit 7b18544085
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Mon Nov 24 13:06:46 2025 +0100

    fix report table

commit 57697a5a39
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Sat Nov 22 00:05:13 2025 +0100

    wip

commit 06fb6c4f3c
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Fri Nov 21 11:21:17 2025 +0100

    wip

commit dd71fd4e11
Author: Carl-Gerhard Lindesvärd <lindesvard@gmail.com>
Date:   Thu Nov 20 13:56:58 2025 +0100

    formulas
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-11-26 12:33:41 +01:00
parent 828c8c4f91
commit b421474616
70 changed files with 6867 additions and 1918 deletions

View File

@@ -7,6 +7,7 @@ import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme';
import { useQuery } from '@tanstack/react-query';
import { isSameDay, isSameHour, isSameMonth, isSameWeek } from 'date-fns';
import { BookmarkIcon, UsersIcon } from 'lucide-react';
import { last } from 'ramda';
import { useCallback } from 'react';
import {
@@ -24,6 +25,10 @@ import {
import { useDashedStroke } from '@/hooks/use-dashed-stroke';
import { useXAxisProps, useYAxisProps } from '../common/axis';
import {
ChartClickMenu,
type ChartClickMenuItem,
} from '../common/chart-click-menu';
import { ReportChartTooltip } from '../common/report-chart-tooltip';
import { ReportTable } from '../common/report-table';
import { SerieIcon } from '../common/serie-icon';
@@ -44,6 +49,8 @@ export function Chart({ data }: Props) {
endDate,
range,
lineType,
series: reportSeries,
breakdowns,
},
isEditMode,
options: { hideXAxis, hideYAxis, maxDomain },
@@ -128,81 +135,146 @@ export function Chart({ data }: Props) {
hide: hideYAxis,
});
const handleChartClick = useCallback((e: any) => {
if (e?.activePayload?.[0]) {
const clickedData = e.activePayload[0].payload;
if (clickedData.date) {
pushModal('AddReference', {
datetime: new Date(clickedData.date).toISOString(),
const getMenuItems = useCallback(
(e: any, clickedData: any): ChartClickMenuItem[] => {
const items: ChartClickMenuItem[] = [];
if (!clickedData?.date) {
return items;
}
// Extract serie ID from the click event if needed
// activePayload is an array of payload objects
const validPayload = e.activePayload?.find(
(p: any) =>
p.dataKey &&
p.dataKey !== 'calcStrokeDasharray' &&
typeof p.dataKey === 'string' &&
p.dataKey.includes(':count'),
);
const serieId = validPayload?.dataKey?.toString().replace(':count', '');
// View Users - only show if we have projectId
if (projectId) {
items.push({
label: 'View Users',
icon: <UsersIcon size={16} />,
onClick: () => {
pushModal('ViewChartUsers', {
type: 'chart',
chartData: data,
report: {
projectId,
series: reportSeries,
breakdowns: breakdowns || [],
interval,
startDate,
endDate,
range,
previous,
chartType: 'linear',
metric: 'sum',
},
date: clickedData.date,
});
},
});
}
}
}, []);
// Add Reference - always show
items.push({
label: 'Add Reference',
icon: <BookmarkIcon size={16} />,
onClick: () => {
pushModal('AddReference', {
datetime: new Date(clickedData.date).toISOString(),
});
},
});
return items;
},
[
projectId,
data,
reportSeries,
breakdowns,
interval,
startDate,
endDate,
range,
previous,
],
);
return (
<ReportChartTooltip.TooltipProvider references={references.data}>
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
<ResponsiveContainer>
<ComposedChart data={rechartData} onClick={handleChartClick}>
<Customized component={calcStrokeDasharray} />
<Line
dataKey="calcStrokeDasharray"
legendType="none"
animationDuration={0}
onAnimationEnd={handleAnimationEnd}
/>
<CartesianGrid
strokeDasharray="3 3"
horizontal={true}
vertical={false}
className="stroke-border"
/>
{references.data?.map((ref) => (
<ReferenceLine
key={ref.id}
x={ref.date.getTime()}
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeDasharray={'3 3'}
label={{
value: ref.title,
position: 'centerTop',
fill: '#334155',
fontSize: 12,
}}
fontSize={10}
<ChartClickMenu getMenuItems={getMenuItems}>
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
<ResponsiveContainer>
<ComposedChart data={rechartData}>
<Customized component={calcStrokeDasharray} />
<Line
dataKey="calcStrokeDasharray"
legendType="none"
animationDuration={0}
onAnimationEnd={handleAnimationEnd}
/>
))}
<YAxis
{...yAxisProps}
domain={maxDomain ? [0, maxDomain] : undefined}
/>
<XAxis {...xAxisProps} />
{series.length > 1 && <Legend content={<CustomLegend />} />}
<Tooltip content={<ReportChartTooltip.Tooltip />} />
{/* {series.map((serie) => {
const color = getChartColor(serie.index);
return (
<React.Fragment key={serie.id}>
<defs>
{isAreaStyle && (
<linearGradient
id={`color${color}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="0%" stopColor={color} stopOpacity={0.8} />
<stop
offset="100%"
stopColor={color}
stopOpacity={0.1}
/>
</linearGradient>
)}
</defs>
<CartesianGrid
strokeDasharray="3 3"
horizontal={true}
vertical={false}
className="stroke-border"
/>
{references.data?.map((ref) => (
<ReferenceLine
key={ref.id}
x={ref.date.getTime()}
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeDasharray={'3 3'}
label={{
value: ref.title,
position: 'centerTop',
fill: '#334155',
fontSize: 12,
}}
fontSize={10}
/>
))}
<YAxis
{...yAxisProps}
domain={maxDomain ? [0, maxDomain] : undefined}
/>
<XAxis {...xAxisProps} />
{series.length > 1 && <Legend content={<CustomLegend />} />}
<Tooltip content={<ReportChartTooltip.Tooltip />} />
<defs>
<filter
id="rainbow-line-glow"
x="-20%"
y="-20%"
width="140%"
height="140%"
>
<feGaussianBlur stdDeviation="5" result="blur" />
<feComponentTransfer in="blur" result="dimmedBlur">
<feFuncA type="linear" slope="0.5" />
</feComponentTransfer>
<feComposite
in="SourceGraphic"
in2="dimmedBlur"
operator="over"
/>
</filter>
</defs>
{series.map((serie) => {
const color = getChartColor(serie.index);
return (
<Line
dot={isAreaStyle && dataLength <= 8}
key={serie.id}
dot={dataLength <= 8}
type={lineType}
name={serie.id}
isAnimationActive={false}
@@ -216,100 +288,46 @@ export function Chart({ data }: Props) {
}
// Use for legend
fill={color}
filter={
series.length === 1
? 'url(#rainbow-line-glow)'
: undefined
}
/>
{previous && (
<Line
type={lineType}
name={`${serie.id}:prev`}
isAnimationActive
dot={false}
strokeOpacity={0.3}
dataKey={`${serie.id}:prev:count`}
stroke={color}
// Use for legend
fill={color}
/>
)}
</React.Fragment>
);
})} */}
);
})}
<defs>
<filter
id="rainbow-line-glow"
x="-20%"
y="-20%"
width="140%"
height="140%"
>
<feGaussianBlur stdDeviation="5" result="blur" />
<feComponentTransfer in="blur" result="dimmedBlur">
<feFuncA type="linear" slope="0.5" />
</feComponentTransfer>
<feComposite
in="SourceGraphic"
in2="dimmedBlur"
operator="over"
/>
</filter>
</defs>
{series.map((serie) => {
const color = getChartColor(serie.index);
return (
<Line
key={serie.id}
dot={dataLength <= 8}
type={lineType}
name={serie.id}
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={color}
strokeDasharray={
useDashedLastLine
? getStrokeDasharray(`${serie.id}:count`)
: undefined
}
// Use for legend
fill={color}
filter={
series.length === 1 ? 'url(#rainbow-line-glow)' : undefined
}
/>
);
})}
{/* Previous */}
{previous
? series.map((serie) => {
const color = getChartColor(serie.index);
return (
<Line
key={`${serie.id}:prev`}
type={lineType}
name={`${serie.id}:prev`}
isAnimationActive
dot={false}
strokeOpacity={0.3}
dataKey={`${serie.id}:prev:count`}
stroke={color}
// Use for legend
fill={color}
/>
);
})
: null}
</ComposedChart>
</ResponsiveContainer>
</div>
{isEditMode && (
<ReportTable
data={data}
visibleSeries={series}
setVisibleSeries={setVisibleSeries}
/>
)}
{/* Previous */}
{previous
? series.map((serie) => {
const color = getChartColor(serie.index);
return (
<Line
key={`${serie.id}:prev`}
type={lineType}
name={`${serie.id}:prev`}
isAnimationActive
dot={false}
strokeOpacity={0.3}
dataKey={`${serie.id}:prev:count`}
stroke={color}
// Use for legend
fill={color}
/>
);
})
: null}
</ComposedChart>
</ResponsiveContainer>
</div>
{isEditMode && (
<ReportTable
data={data}
visibleSeries={series}
setVisibleSeries={setVisibleSeries}
/>
)}
</ChartClickMenu>
</ReportChartTooltip.TooltipProvider>
);
}