Clear Overview
This commit is contained in:
parent
b88bdc0896
commit
9ee76a0742
@ -1,99 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import BaseButton from '../BaseButton';
|
|
||||||
import BaseIcon from '../BaseIcon';
|
|
||||||
import * as icons from '@mdi/js';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
|
||||||
|
|
||||||
import { fetchWidgets, removeWidget } from '../../stores/roles/rolesSlice';
|
|
||||||
import { WidgetChartType, WidgetType } from "./models/widget.model";
|
|
||||||
import { BarChart } from "./components/BarChart";
|
|
||||||
import { PieChart } from "./components/PieChart";
|
|
||||||
import { AreaChart } from "./components/AreaChart";
|
|
||||||
import { LineChart } from "./components/LineChart";
|
|
||||||
|
|
||||||
export const SmartWidget = ({ widget, userId, admin, roleId }) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
|
||||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
|
||||||
|
|
||||||
const deleteWidget = async () => {
|
|
||||||
await dispatch(
|
|
||||||
removeWidget({ id: userId, widgetId: widget.widget_id, roleId }),
|
|
||||||
);
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
corners !== 'rounded-full' ? corners : 'rounded-3xl'
|
|
||||||
} dark:bg-dark-900 dark:border-dark-700 p-6 ${cardsStyle} ${
|
|
||||||
widget.widget_type === 'chart'
|
|
||||||
? 'col-span-2'
|
|
||||||
: 'lg:col-span-1 col-span-2'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className='flex justify-between flex-col h-full'>
|
|
||||||
<div className='flex justify-between items-center'>
|
|
||||||
<div className='text-lg leading-tight text-gray-500 dark:text-dark-600 line-clamp-2'>
|
|
||||||
{widget.label}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{admin && (
|
|
||||||
<BaseButton
|
|
||||||
icon={icons.mdiClose}
|
|
||||||
color='whiteDark'
|
|
||||||
roundedFull
|
|
||||||
onClick={deleteWidget}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-center items-center h-full'>
|
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
widget.widget_type === WidgetType.chart ? 'w-5/6 justify-center' : ''
|
|
||||||
} items-center flex flex-grow justify-center`}
|
|
||||||
>
|
|
||||||
{widget.value ? (
|
|
||||||
widget.widget_type === WidgetType.chart ? (
|
|
||||||
widget.chart_type === WidgetChartType.bar ? (
|
|
||||||
<BarChart widget={widget}/>
|
|
||||||
) : widget.chart_type === WidgetChartType.line ? (
|
|
||||||
<LineChart widget={widget}/>
|
|
||||||
) : widget.chart_type === WidgetChartType.pie ? (
|
|
||||||
<PieChart widget={widget}/>
|
|
||||||
) : widget.chart_type === WidgetChartType.area ? (
|
|
||||||
<AreaChart widget={widget}/>
|
|
||||||
) : widget.chart_type === WidgetChartType.funnel ? (
|
|
||||||
<AreaChart widget={widget}/>
|
|
||||||
) : null
|
|
||||||
) : (
|
|
||||||
<div className='text-3xl leading-tight font-semibold truncate'>
|
|
||||||
{widget.value}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className='text-center text-red-400'>
|
|
||||||
Something went wrong, please try again or use a different query.
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{widget.type === WidgetType.scalar && widget.mdiIcon && (
|
|
||||||
<div className='flex justify-end w-1/4'>
|
|
||||||
<BaseIcon
|
|
||||||
className='text-blue-500 flex-shrink-0'
|
|
||||||
w='w-16'
|
|
||||||
h='h-16'
|
|
||||||
size={48}
|
|
||||||
fill={widget.color}
|
|
||||||
path={icons[widget.mdiIcon]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { WidgetLibName } from '../models/widget.model';
|
|
||||||
import { ApexAreaChart } from './AreaChart/ApexAreaChart';
|
|
||||||
import { ChartJSAreaChart } from './AreaChart/ChartJSAreaChart';
|
|
||||||
|
|
||||||
export const AreaChart = ({ widget }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!widget.lib_name && <ChartJSAreaChart widget={widget} />}
|
|
||||||
{widget.lib_name === WidgetLibName.chartjs && (
|
|
||||||
<ChartJSAreaChart widget={widget} />
|
|
||||||
)}
|
|
||||||
{widget.lib_name === WidgetLibName.apex && (
|
|
||||||
<ApexAreaChart widget={widget} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
|
|
||||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
|
||||||
type ValueType = { [key: string]: string | number }[];
|
|
||||||
|
|
||||||
export const ApexAreaChart = ({ widget }) => {
|
|
||||||
const dataForLineChart = (value: any[]) => {
|
|
||||||
if (!value?.length || value?.length > 10000)
|
|
||||||
return [{ name: '', data: [] }];
|
|
||||||
|
|
||||||
const valueKey = Object.keys(value[0])[1];
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
|
|
||||||
return [{ name: humanize(valueKey), data }];
|
|
||||||
};
|
|
||||||
|
|
||||||
const optionsForLineChart = (
|
|
||||||
value: ValueType,
|
|
||||||
chartColor: string[],
|
|
||||||
currency: boolean,
|
|
||||||
) => {
|
|
||||||
const chartColors = Array.isArray(chartColor)
|
|
||||||
? chartColor
|
|
||||||
: [chartColor || '#3751FF'];
|
|
||||||
const defaultOptions = {
|
|
||||||
xaxis: {},
|
|
||||||
chart: {
|
|
||||||
toolbar: {
|
|
||||||
show: true,
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
tools: {
|
|
||||||
download: true,
|
|
||||||
selection: true,
|
|
||||||
zoom: true,
|
|
||||||
zoomin: true,
|
|
||||||
zoomout: true,
|
|
||||||
pan: true,
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
csv: {
|
|
||||||
filename: undefined,
|
|
||||||
columnDelimiter: ',',
|
|
||||||
headerCategory: 'category',
|
|
||||||
headerValue: 'value',
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
png: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
distributed: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!value?.length || value?.length > 10000) return defaultOptions;
|
|
||||||
|
|
||||||
const key = Object.keys(value[0])[0];
|
|
||||||
const categories = value
|
|
||||||
.map((el) => el[key])
|
|
||||||
.map((item) =>
|
|
||||||
typeof item === 'string' && item?.length > 7
|
|
||||||
? item?.slice(0, 7)
|
|
||||||
: item || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories.length <= 3) {
|
|
||||||
defaultOptions.plotOptions = {
|
|
||||||
bar: {
|
|
||||||
distributed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [];
|
|
||||||
for (let i = 0; i < categories.length; i++) {
|
|
||||||
colors.push(chartColors[i % chartColors.length]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function (value) {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + value;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
formatter: (val) => {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + val;
|
|
||||||
} else {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories,
|
|
||||||
},
|
|
||||||
colors,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
options={optionsForLineChart(
|
|
||||||
widget.value,
|
|
||||||
widget.color_array,
|
|
||||||
widget.currency,
|
|
||||||
)}
|
|
||||||
series={dataForLineChart(widget.value)}
|
|
||||||
type={widget.chart_type}
|
|
||||||
height={200}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,96 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers';
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
ChartData,
|
|
||||||
} from 'chart.js';
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
PointElement,
|
|
||||||
LineElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Filler,
|
|
||||||
Legend,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ChartJSAreaChart = ({ widget }) => {
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataForBarChart = (
|
|
||||||
value: any[],
|
|
||||||
chartColors: string[],
|
|
||||||
): ChartData<'line', number[], string> => {
|
|
||||||
if (!value?.length) return { labels: [''], datasets: [{ data: [] }] };
|
|
||||||
const initColors = Array.isArray(chartColors)
|
|
||||||
? chartColors
|
|
||||||
: [chartColors || '#3751FF'];
|
|
||||||
|
|
||||||
const valueKey = findFirstNumericKey(value[0]);
|
|
||||||
const label = humanize(valueKey);
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
const labels = value.map((el) =>
|
|
||||||
Object.keys(el).length <= 2
|
|
||||||
? humanize(String(el[Object.keys(el)[0]]))
|
|
||||||
: collectOtherData(el, valueKey),
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundColor =
|
|
||||||
labels.length > initColors.length
|
|
||||||
? chroma
|
|
||||||
.scale([
|
|
||||||
chroma(initColors[0]).brighten(),
|
|
||||||
chroma(initColors.slice(-1)[0]).darken(),
|
|
||||||
])
|
|
||||||
.colors(labels.length)
|
|
||||||
: initColors;
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
data,
|
|
||||||
backgroundColor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Line
|
|
||||||
data={dataForBarChart(widget.value, widget.color_array)}
|
|
||||||
options={options}
|
|
||||||
height={350}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { ChartJSBarChart } from './BarChart/ChartJSBarChart';
|
|
||||||
import { ApexBarChart } from './BarChart/ApexBarChart';
|
|
||||||
import { WidgetLibName } from '../models/widget.model';
|
|
||||||
|
|
||||||
export const BarChart = ({ widget }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!widget.lib_name && <ChartJSBarChart widget={widget} />}
|
|
||||||
{widget.lib_name === WidgetLibName.chartjs && (
|
|
||||||
<ChartJSBarChart widget={widget} />
|
|
||||||
)}
|
|
||||||
{widget.lib_name === WidgetLibName.apex && (
|
|
||||||
<ApexBarChart widget={widget} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
|
|
||||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
|
||||||
type ValueType = { [key: string]: string | number }[];
|
|
||||||
|
|
||||||
export const ApexBarChart = ({ widget }) => {
|
|
||||||
const dataForBarChart = (value: any[]) => {
|
|
||||||
if (!value?.length || value?.length > 10000)
|
|
||||||
return [{ name: '', data: [] }];
|
|
||||||
|
|
||||||
const valueKey = Object.keys(value[0])[1];
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
|
|
||||||
return [{ name: humanize(valueKey), data }];
|
|
||||||
};
|
|
||||||
const optionsForBarChart = (
|
|
||||||
value: ValueType,
|
|
||||||
chartColor: string[],
|
|
||||||
currency: boolean,
|
|
||||||
) => {
|
|
||||||
const chartColors = Array.isArray(chartColor)
|
|
||||||
? chartColor
|
|
||||||
: [chartColor || '#3751FF'];
|
|
||||||
const defaultOptions = {
|
|
||||||
xaxis: {},
|
|
||||||
chart: {
|
|
||||||
toolbar: {
|
|
||||||
show: true,
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
tools: {
|
|
||||||
download: true,
|
|
||||||
selection: true,
|
|
||||||
zoom: true,
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
csv: {
|
|
||||||
filename: undefined,
|
|
||||||
columnDelimiter: ',',
|
|
||||||
headerCategory: 'category',
|
|
||||||
headerValue: 'value',
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
png: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
distributed: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!value?.length || value?.length > 10000) return defaultOptions;
|
|
||||||
|
|
||||||
const key = Object.keys(value[0])[0];
|
|
||||||
const categories = value
|
|
||||||
.map((el) => el[key])
|
|
||||||
.map((item) =>
|
|
||||||
typeof item === 'string' && item?.length > 20
|
|
||||||
? item?.slice(0, 15)
|
|
||||||
: item || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories.length <= 3) {
|
|
||||||
defaultOptions.plotOptions = {
|
|
||||||
bar: {
|
|
||||||
distributed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [];
|
|
||||||
for (let i = 0; i < categories.length; i++) {
|
|
||||||
colors.push(chartColors[i % chartColors.length]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function (value) {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + value;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
formatter: (val) => {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + val;
|
|
||||||
} else {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories,
|
|
||||||
},
|
|
||||||
colors,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
options={optionsForBarChart(
|
|
||||||
widget.value,
|
|
||||||
widget.color_array,
|
|
||||||
widget.currency,
|
|
||||||
)}
|
|
||||||
series={dataForBarChart(widget.value)}
|
|
||||||
type={widget.chart_type}
|
|
||||||
height={300}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
import { Bar } from 'react-chartjs-2';
|
|
||||||
import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers';
|
|
||||||
import {
|
|
||||||
BarElement,
|
|
||||||
CategoryScale,
|
|
||||||
Chart as ChartJS,
|
|
||||||
ChartData,
|
|
||||||
Legend,
|
|
||||||
LinearScale,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
} from 'chart.js';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
|
|
||||||
ChartJS.register(
|
|
||||||
CategoryScale,
|
|
||||||
LinearScale,
|
|
||||||
BarElement,
|
|
||||||
Title,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const ChartJSBarChart = ({ widget }) => {
|
|
||||||
console.log(widget)
|
|
||||||
const options = () => {
|
|
||||||
return {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'top' as const,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: widget.label,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataForBarChart = (
|
|
||||||
value: any[],
|
|
||||||
chartColors: string[],
|
|
||||||
): ChartData<'bar', number[], string> => {
|
|
||||||
if (!value?.length) return { labels: [''], datasets: [{ data: [] }] };
|
|
||||||
|
|
||||||
const initColors = Array.isArray(chartColors)
|
|
||||||
? chartColors
|
|
||||||
: [chartColors || '#3751FF'];
|
|
||||||
|
|
||||||
const valueKey = findFirstNumericKey(value[0]);
|
|
||||||
const label = humanize(valueKey);
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
const labels = value.map((el) =>
|
|
||||||
Object.keys(el).length <= 2
|
|
||||||
? humanize(String(el[Object.keys(el)[0]]))
|
|
||||||
: collectOtherData(el, valueKey),
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundColor =
|
|
||||||
labels.length > initColors.length
|
|
||||||
? chroma
|
|
||||||
.scale([
|
|
||||||
chroma(initColors[0]).brighten(),
|
|
||||||
chroma(initColors.slice(-1)[0]).darken(),
|
|
||||||
])
|
|
||||||
.colors(labels.length)
|
|
||||||
: initColors;
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
data,
|
|
||||||
backgroundColor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Bar
|
|
||||||
data={dataForBarChart(widget.value, widget.color_array)}
|
|
||||||
options={options()}
|
|
||||||
height={350}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { humanize } from '../../../helpers/humanize';
|
|
||||||
|
|
||||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
|
||||||
type ValueType = { [key: string]: string | number }[];
|
|
||||||
|
|
||||||
export const FunnelChart = ({ widget }) => {
|
|
||||||
const dataForBarChart = (value: any[]) => {
|
|
||||||
if (!value?.length || value?.length > 10000)
|
|
||||||
return [{ name: '', data: [] }];
|
|
||||||
const valueKey = Object.keys(value[0])[1];
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
|
|
||||||
return [{ name: humanize(valueKey), data }];
|
|
||||||
};
|
|
||||||
const optionsForBarChart = (
|
|
||||||
value: ValueType,
|
|
||||||
chartColor: string[],
|
|
||||||
currency: boolean,
|
|
||||||
) => {
|
|
||||||
const chartColors = Array.isArray(chartColor)
|
|
||||||
? chartColor
|
|
||||||
: [chartColor || '#3751FF'];
|
|
||||||
const defaultOptions = {
|
|
||||||
xaxis: {},
|
|
||||||
chart: {
|
|
||||||
toolbar: {
|
|
||||||
show: true,
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
tools: {
|
|
||||||
download: true,
|
|
||||||
selection: true,
|
|
||||||
zoom: true,
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
csv: {
|
|
||||||
filename: undefined,
|
|
||||||
columnDelimiter: ',',
|
|
||||||
headerCategory: 'category',
|
|
||||||
headerValue: 'value',
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
png: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
distributed: false,
|
|
||||||
horizontal: true,
|
|
||||||
isFunnel: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!value?.length || value?.length > 10000) return defaultOptions;
|
|
||||||
|
|
||||||
const key = Object.keys(value[0])[0];
|
|
||||||
const categories = value
|
|
||||||
.map((el) => el[key])
|
|
||||||
.map((item) =>
|
|
||||||
typeof item === 'string' && item?.length > 20
|
|
||||||
? item?.slice(0, 15)
|
|
||||||
: item || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories.length <= 3) {
|
|
||||||
defaultOptions.plotOptions = {
|
|
||||||
bar: {
|
|
||||||
distributed: true,
|
|
||||||
horizontal: true,
|
|
||||||
isFunnel: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [];
|
|
||||||
for (let i = 0; i < categories.length; i++) {
|
|
||||||
colors.push(chartColors[i % chartColors.length]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function (value) {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + value;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
formatter: (val) => {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + val;
|
|
||||||
} else {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: true,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories,
|
|
||||||
},
|
|
||||||
colors,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
options={optionsForBarChart(
|
|
||||||
widget.value,
|
|
||||||
widget.color_array,
|
|
||||||
widget.currency,
|
|
||||||
)}
|
|
||||||
series={dataForBarChart(widget.value)}
|
|
||||||
type={widget.chart_type}
|
|
||||||
height={300}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { WidgetLibName } from '../models/widget.model';
|
|
||||||
import { ApexLineChart } from './LineChart/ApexLineChart';
|
|
||||||
import { ChartJSLineChart } from './LineChart/ChartJSLineChart';
|
|
||||||
|
|
||||||
export const LineChart = ({ widget }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!widget.lib_name && <ChartJSLineChart widget={widget} />}
|
|
||||||
{widget.lib_name === WidgetLibName.chartjs && (
|
|
||||||
<ChartJSLineChart widget={widget} />
|
|
||||||
)}
|
|
||||||
{widget.lib_name === WidgetLibName.apex && (
|
|
||||||
<ApexLineChart widget={widget} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
|
|
||||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
|
||||||
type ValueType = { [key: string]: string | number }[];
|
|
||||||
|
|
||||||
export const ApexLineChart = ({ widget }) => {
|
|
||||||
const dataForLineChart = (value: any[]) => {
|
|
||||||
if (!value?.length || value?.length > 10000)
|
|
||||||
return [{ name: '', data: [] }];
|
|
||||||
|
|
||||||
const valueKey = Object.keys(value[0])[1];
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
|
|
||||||
return [{ name: humanize(valueKey), data }];
|
|
||||||
};
|
|
||||||
|
|
||||||
const optionsForLineChart = (
|
|
||||||
value: ValueType,
|
|
||||||
chartColor: string[],
|
|
||||||
currency: boolean,
|
|
||||||
) => {
|
|
||||||
const chartColors = Array.isArray(chartColor)
|
|
||||||
? chartColor
|
|
||||||
: [chartColor || '#3751FF'];
|
|
||||||
const defaultOptions = {
|
|
||||||
xaxis: {},
|
|
||||||
chart: {
|
|
||||||
toolbar: {
|
|
||||||
show: true,
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
tools: {
|
|
||||||
download: true,
|
|
||||||
selection: true,
|
|
||||||
zoom: true,
|
|
||||||
zoomin: true,
|
|
||||||
zoomout: true,
|
|
||||||
pan: true,
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
csv: {
|
|
||||||
filename: undefined,
|
|
||||||
columnDelimiter: ',',
|
|
||||||
headerCategory: 'category',
|
|
||||||
headerValue: 'value',
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
png: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plotOptions: {
|
|
||||||
bar: {
|
|
||||||
distributed: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
colors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!value?.length || value?.length > 10000) return defaultOptions;
|
|
||||||
|
|
||||||
const key = Object.keys(value[0])[0];
|
|
||||||
const categories = value
|
|
||||||
.map((el) => el[key])
|
|
||||||
.map((item) =>
|
|
||||||
typeof item === 'string' && item?.length > 7
|
|
||||||
? item?.slice(0, 7)
|
|
||||||
: item || '',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (categories.length <= 3) {
|
|
||||||
defaultOptions.plotOptions = {
|
|
||||||
bar: {
|
|
||||||
distributed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const colors = [];
|
|
||||||
for (let i = 0; i < categories.length; i++) {
|
|
||||||
colors.push(chartColors[i % chartColors.length]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
yaxis: {
|
|
||||||
labels: {
|
|
||||||
formatter: function (value) {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + value;
|
|
||||||
} else {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
dataLabels: {
|
|
||||||
formatter: (val) => {
|
|
||||||
if (currency) {
|
|
||||||
return '$' + val;
|
|
||||||
} else {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
legend: {
|
|
||||||
show: false,
|
|
||||||
},
|
|
||||||
xaxis: {
|
|
||||||
categories,
|
|
||||||
},
|
|
||||||
colors,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
options={optionsForLineChart(
|
|
||||||
widget.value,
|
|
||||||
widget.color_array,
|
|
||||||
widget.currency,
|
|
||||||
)}
|
|
||||||
series={dataForLineChart(widget.value)}
|
|
||||||
type={widget.chart_type}
|
|
||||||
height={200}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,97 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
import { Line } from 'react-chartjs-2';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers';
|
|
||||||
import { Widget } from '../../models/widget.model';
|
|
||||||
import {
|
|
||||||
Chart,
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
LineController,
|
|
||||||
LinearScale,
|
|
||||||
CategoryScale,
|
|
||||||
Tooltip,
|
|
||||||
ChartData,
|
|
||||||
} from 'chart.js';
|
|
||||||
|
|
||||||
Chart.register(
|
|
||||||
LineElement,
|
|
||||||
PointElement,
|
|
||||||
LineController,
|
|
||||||
LinearScale,
|
|
||||||
CategoryScale,
|
|
||||||
Tooltip,
|
|
||||||
);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
widget: Widget;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChartJSLineChart = (props: Props) => {
|
|
||||||
const options = {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
scales: {
|
|
||||||
y: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
x: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataForBarChart = (
|
|
||||||
value: any[],
|
|
||||||
chartColors: string[],
|
|
||||||
): ChartData<'line', number[], string> => {
|
|
||||||
if (!value?.length) return { labels: [''], datasets: [{ data: [] }] };
|
|
||||||
const initColors = Array.isArray(chartColors)
|
|
||||||
? chartColors
|
|
||||||
: [chartColors || '#3751FF'];
|
|
||||||
|
|
||||||
const valueKey = findFirstNumericKey(value[0]);
|
|
||||||
const label = humanize(valueKey);
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
const labels = value.map((el) =>
|
|
||||||
Object.keys(el).length <= 2
|
|
||||||
? humanize(String(el[Object.keys(el)[0]]))
|
|
||||||
: collectOtherData(el, valueKey),
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundColor =
|
|
||||||
labels.length > initColors.length
|
|
||||||
? chroma
|
|
||||||
.scale([
|
|
||||||
chroma(initColors[0]).brighten(),
|
|
||||||
chroma(initColors.slice(-1)[0]).darken(),
|
|
||||||
])
|
|
||||||
.colors(labels.length)
|
|
||||||
: initColors;
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
data,
|
|
||||||
backgroundColor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Line
|
|
||||||
data={dataForBarChart(props.widget.value, props.widget.color_array)}
|
|
||||||
options={options}
|
|
||||||
height={350}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { WidgetLibName } from '../models/widget.model';
|
|
||||||
import { ApexPieChart } from './PieChart/ApexPieChart';
|
|
||||||
import { ChartJSPieChart } from './PieChart/ChartJSPieChart';
|
|
||||||
|
|
||||||
export const PieChart = ({ widget }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!widget.lib_name && <ChartJSPieChart widget={widget} />}
|
|
||||||
{widget.lib_name === WidgetLibName.chartjs && (
|
|
||||||
<ChartJSPieChart widget={widget} />
|
|
||||||
)}
|
|
||||||
{widget.lib_name === WidgetLibName.apex && (
|
|
||||||
<ApexPieChart widget={widget} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
|
|
||||||
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
|
|
||||||
type ValueType = { [key: string]: string | number }[];
|
|
||||||
|
|
||||||
export const ApexPieChart = ({ widget }) => {
|
|
||||||
const optionsForPieChart = (value: ValueType, chartColor: string) => {
|
|
||||||
const chartColors = Array.isArray(chartColor)
|
|
||||||
? chartColor
|
|
||||||
: [chartColor || '#3751FF'];
|
|
||||||
const defaultOptions = {
|
|
||||||
xaxis: {},
|
|
||||||
toolbar: {
|
|
||||||
show: true,
|
|
||||||
offsetX: 0,
|
|
||||||
offsetY: 0,
|
|
||||||
tools: {
|
|
||||||
download: true,
|
|
||||||
selection: true,
|
|
||||||
zoom: true,
|
|
||||||
zoomin: true,
|
|
||||||
zoomout: true,
|
|
||||||
pan: true,
|
|
||||||
customIcons: [],
|
|
||||||
},
|
|
||||||
export: {
|
|
||||||
csv: {
|
|
||||||
filename: undefined,
|
|
||||||
columnDelimiter: ',',
|
|
||||||
headerCategory: 'category',
|
|
||||||
headerValue: 'value',
|
|
||||||
},
|
|
||||||
svg: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
png: {
|
|
||||||
filename: undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
autoSelected: 'zoom',
|
|
||||||
},
|
|
||||||
colors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!value?.length || value?.length > 10000) return defaultOptions;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isNaN(Number(value[0][Object.keys(value[0])[1]])) &&
|
|
||||||
isFinite(Number(value[0][Object.keys(value[0])[1]]))
|
|
||||||
) {
|
|
||||||
const labels = value
|
|
||||||
.map((el) => String(el[Object.keys(value[0])[0]]))
|
|
||||||
.reverse();
|
|
||||||
|
|
||||||
let colors: string[] | (string & any[]);
|
|
||||||
if (labels.length > chartColors.length) {
|
|
||||||
colors = chroma
|
|
||||||
.scale([
|
|
||||||
chroma(chartColors.at(0)).brighten(),
|
|
||||||
chroma(chartColors.at(-1)).darken(),
|
|
||||||
])
|
|
||||||
.colors(labels.length);
|
|
||||||
} else {
|
|
||||||
colors = chartColors;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
colors,
|
|
||||||
labels,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const key = Object.keys(value[0])[1];
|
|
||||||
const categories = value.map((el) => String(el[key])).reverse();
|
|
||||||
|
|
||||||
return {
|
|
||||||
...defaultOptions,
|
|
||||||
labels: categories,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const dataForPieChart = (value: any[]) => {
|
|
||||||
if (!value?.length || value?.length > 10000)
|
|
||||||
return [{ name: '', data: [] }];
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isNaN(parseFloat(value[0][Object.keys(value[0])[1]])) &&
|
|
||||||
isFinite(value[0][Object.keys(value[0])[1]])
|
|
||||||
) {
|
|
||||||
return value.map((el) => +el[Object.keys(value[0])[1]]).reverse();
|
|
||||||
}
|
|
||||||
const valueKey = Object.keys(value[0])[0];
|
|
||||||
return value.map((el) => +el[valueKey]).reverse();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Chart
|
|
||||||
options={optionsForPieChart(widget.value, widget.color_array)}
|
|
||||||
series={dataForPieChart(widget.value)}
|
|
||||||
type={widget.chart_type}
|
|
||||||
height={200}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,81 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { humanize } from '../../../../helpers/humanize';
|
|
||||||
import { Pie } from 'react-chartjs-2';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
import { collectOtherData, findFirstNumericKey } from '../../widgetHelpers';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
|
||||||
ArcElement,
|
|
||||||
Tooltip,
|
|
||||||
Legend,
|
|
||||||
ChartData,
|
|
||||||
} from 'chart.js';
|
|
||||||
|
|
||||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
|
||||||
|
|
||||||
export const ChartJSPieChart = ({ widget }) => {
|
|
||||||
const options = () => {
|
|
||||||
return {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'right' as const,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: true,
|
|
||||||
text: widget.label,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const dataForBarChart = (
|
|
||||||
value: any[],
|
|
||||||
chartColors: string[],
|
|
||||||
): ChartData<'pie', number[], string> => {
|
|
||||||
if (!value?.length) return { labels: [''], datasets: [{ data: [] }] };
|
|
||||||
const initColors = Array.isArray(chartColors)
|
|
||||||
? chartColors
|
|
||||||
: [chartColors || '#3751FF'];
|
|
||||||
|
|
||||||
const valueKey = findFirstNumericKey(value[0]);
|
|
||||||
const label = humanize(valueKey);
|
|
||||||
const data = value.map((el) => +el[valueKey]);
|
|
||||||
const labels = value.map((el) =>
|
|
||||||
Object.keys(el).length <= 2
|
|
||||||
? humanize(String(el[Object.keys(el)[0]]))
|
|
||||||
: collectOtherData(el, valueKey),
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundColor =
|
|
||||||
labels.length > initColors.length
|
|
||||||
? chroma
|
|
||||||
.scale([
|
|
||||||
chroma(initColors[0]).brighten(),
|
|
||||||
chroma(initColors.slice(-1)[0]).darken(),
|
|
||||||
])
|
|
||||||
.colors(labels.length)
|
|
||||||
: initColors;
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label,
|
|
||||||
data,
|
|
||||||
backgroundColor,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pie
|
|
||||||
data={dataForBarChart(widget.value, widget.color_array)}
|
|
||||||
options={options()}
|
|
||||||
height={350}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
export enum WidgetLibName {
|
|
||||||
apex = 'apex',
|
|
||||||
chartjs = 'chartjs',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WidgetChartType {
|
|
||||||
scalar = 'scalar',
|
|
||||||
bar = 'bar',
|
|
||||||
line = 'line',
|
|
||||||
pie = 'pie',
|
|
||||||
area = 'area',
|
|
||||||
funnel = 'funnel',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum WidgetType {
|
|
||||||
chart = 'chart',
|
|
||||||
scalar = 'scalar',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Widget {
|
|
||||||
type: WidgetType;
|
|
||||||
chartType: WidgetChartType;
|
|
||||||
query: string;
|
|
||||||
mdiIcon: string;
|
|
||||||
iconColor: string;
|
|
||||||
label: string;
|
|
||||||
id: string;
|
|
||||||
lib?: WidgetLibName;
|
|
||||||
value: any[];
|
|
||||||
chartColors: string[];
|
|
||||||
options?: any;
|
|
||||||
prompt: string;
|
|
||||||
color: string;
|
|
||||||
color_array: string[];
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import { humanize } from '../../helpers/humanize';
|
|
||||||
|
|
||||||
interface DataObject {
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findFirstNumericKey = (obj: Record<string, any>): string | undefined => {
|
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmedValue = value.trim();
|
|
||||||
|
|
||||||
// Only allow numbers, and optionally a single decimal point
|
|
||||||
const isNumeric = /^-?\d+(\.\d+)?$/.test(trimmedValue);
|
|
||||||
|
|
||||||
if (isNumeric) {
|
|
||||||
// Check if the number is the same as the trimmed value
|
|
||||||
// This is to avoid cases like '1.0' being treated as a number
|
|
||||||
const numberValue = parseFloat(trimmedValue);
|
|
||||||
if (numberValue.toString() === trimmedValue) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const collectOtherData = (
|
|
||||||
obj: DataObject,
|
|
||||||
excludeKey: string,
|
|
||||||
): string => {
|
|
||||||
return Object.entries(obj)
|
|
||||||
.filter(([key, _]) => key !== excludeKey)
|
|
||||||
.map(([_, value]) => humanize(value))
|
|
||||||
.join(' / ');
|
|
||||||
};
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
import React, { useEffect, useId, useState } from 'react';
|
|
||||||
import { AsyncPaginate } from 'react-select-async-paginate';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
export const RoleSelect = ({ options, field, form, itemRef, disabled, currentUser }) => {
|
|
||||||
const [value, setValue] = useState(null);
|
|
||||||
const PAGE_SIZE = 100;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (currentUser.app_role.id) {
|
|
||||||
setValue({ value: currentUser.app_role.id, label: currentUser.app_role.name });
|
|
||||||
}
|
|
||||||
}, [currentUser]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (options?.value && options?.label) {
|
|
||||||
setValue({ value: options.value, label: options.label });
|
|
||||||
}
|
|
||||||
}, [options?.id, field?.value?.id]);
|
|
||||||
|
|
||||||
const mapResponseToValuesAndLabels = (data) => ({
|
|
||||||
value: data.id,
|
|
||||||
label: data.label,
|
|
||||||
});
|
|
||||||
const handleChange = (option) => {
|
|
||||||
form.setFieldValue(field.name, option);
|
|
||||||
setValue(option);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function callApi(inputValue: string, loadedOptions: any[]) {
|
|
||||||
const path = `/${itemRef}/autocomplete?limit=${PAGE_SIZE}&offset=${loadedOptions.length}${inputValue ? `&query=${inputValue}` : ''}`;
|
|
||||||
const { data } = await axios(path);
|
|
||||||
return {
|
|
||||||
options: data.map(mapResponseToValuesAndLabels),
|
|
||||||
hasMore: data.length === PAGE_SIZE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<AsyncPaginate
|
|
||||||
classNames={{
|
|
||||||
control: () => 'px-1 py-2',
|
|
||||||
}}
|
|
||||||
classNamePrefix={'react-select'}
|
|
||||||
instanceId={useId()}
|
|
||||||
value={value}
|
|
||||||
debounceTimeout={1000}
|
|
||||||
loadOptions={callApi}
|
|
||||||
onChange={handleChange}
|
|
||||||
defaultOptions
|
|
||||||
isDisabled={disabled}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,140 +0,0 @@
|
|||||||
import CardBox from '../CardBox';
|
|
||||||
import { mdiCog } from '@mdi/js';
|
|
||||||
import { Field, Form, Formik } from 'formik';
|
|
||||||
import { ToastContainer, toast } from 'react-toastify';
|
|
||||||
import FormField from '../FormField';
|
|
||||||
import React from 'react';
|
|
||||||
import {
|
|
||||||
aiPrompt,
|
|
||||||
setErrorNotification,
|
|
||||||
resetNotify,
|
|
||||||
} from '../../stores/openAiSlice';
|
|
||||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
|
||||||
|
|
||||||
import { fetchWidgets } from '../../stores/roles/rolesSlice';
|
|
||||||
|
|
||||||
import BaseButton from '../BaseButton';
|
|
||||||
import CardBoxModal from '../CardBoxModal';
|
|
||||||
import { RoleSelect } from './RoleSelect';
|
|
||||||
|
|
||||||
export const WidgetCreator = ({
|
|
||||||
currentUser,
|
|
||||||
isFetchingQuery,
|
|
||||||
setWidgetsRole,
|
|
||||||
widgetsRole,
|
|
||||||
}) => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
||||||
const { notify: openAiNotify } = useAppSelector((state) => state.openAi);
|
|
||||||
|
|
||||||
const notify = (type, msg) => toast(msg, { type, position: 'bottom-center' });
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (openAiNotify.showNotification) {
|
|
||||||
notify(openAiNotify.typeNotification, openAiNotify.textNotification);
|
|
||||||
dispatch(resetNotify());
|
|
||||||
}
|
|
||||||
}, [openAiNotify.showNotification]);
|
|
||||||
|
|
||||||
const openModal = (): void => {
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseModal = (value = {}) => {
|
|
||||||
setWidgetsRole(value);
|
|
||||||
setIsModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWidgets = async () => {
|
|
||||||
await dispatch(fetchWidgets(widgetsRole?.role?.value || ''));
|
|
||||||
};
|
|
||||||
|
|
||||||
const smartSearch = async (values: { description: string }, resetForm: any) => {
|
|
||||||
const description = values.description;
|
|
||||||
const projectId = '';
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
roleId: widgetsRole?.role?.value,
|
|
||||||
description,
|
|
||||||
projectId,
|
|
||||||
userId: currentUser?.id,
|
|
||||||
};
|
|
||||||
const { payload: responcePayload, error }: any = await dispatch(aiPrompt(payload));
|
|
||||||
|
|
||||||
await getWidgets().then();
|
|
||||||
|
|
||||||
resetForm({ values: { description: '' } });
|
|
||||||
if (responcePayload.data?.error || error) {
|
|
||||||
const errorMessage =
|
|
||||||
responcePayload.data?.error?.message || error?.message;
|
|
||||||
await dispatch(
|
|
||||||
setErrorNotification(errorMessage || 'Error with widget creation'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CardBox id="widgetCreator" className='mb-6 relative'>
|
|
||||||
<BaseButton
|
|
||||||
className='absolute top-0 right-0 m-4'
|
|
||||||
icon={mdiCog}
|
|
||||||
color='whiteDark'
|
|
||||||
roundedFull
|
|
||||||
onClick={openModal}
|
|
||||||
/>
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
description: '',
|
|
||||||
}}
|
|
||||||
onSubmit={(values, { resetForm }) => smartSearch(values, resetForm)}
|
|
||||||
>
|
|
||||||
<Form>
|
|
||||||
<FormField
|
|
||||||
label='Create Chart or Widget'
|
|
||||||
help={
|
|
||||||
isFetchingQuery ?
|
|
||||||
'Loading...' :
|
|
||||||
'Describe your new widget or chart in natural language. For example: "Number of admin users" OR "red chart with number of closed contracts grouped by month"'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Field type='input' name='description' disabled={isFetchingQuery} />
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
|
||||||
</Formik>
|
|
||||||
</CardBox>
|
|
||||||
<Formik
|
|
||||||
initialValues={{
|
|
||||||
role: '',
|
|
||||||
}}
|
|
||||||
onSubmit={(values) => handleCloseModal(values)}
|
|
||||||
>
|
|
||||||
{({ submitForm }) => (
|
|
||||||
<CardBoxModal
|
|
||||||
title='Widget Creator Settings'
|
|
||||||
buttonColor='info'
|
|
||||||
buttonLabel='Done'
|
|
||||||
isActive={isModalOpen}
|
|
||||||
onConfirm={submitForm}
|
|
||||||
onCancel={() => setIsModalOpen(false)}
|
|
||||||
>
|
|
||||||
<p>What role are we showing and creating widgets for?</p>
|
|
||||||
|
|
||||||
<Form>
|
|
||||||
<FormField>
|
|
||||||
<Field
|
|
||||||
name='role'
|
|
||||||
id='role'
|
|
||||||
component={RoleSelect}
|
|
||||||
options={widgetsRole?.role || []}
|
|
||||||
itemRef={'roles'}
|
|
||||||
currentUser={currentUser}
|
|
||||||
></Field>
|
|
||||||
</FormField>
|
|
||||||
</Form>
|
|
||||||
</CardBoxModal>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
<ToastContainer />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -1,94 +1,30 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js';
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import axios from 'axios';
|
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated'
|
import LayoutAuthenticated from '../layouts/Authenticated'
|
||||||
import SectionMain from '../components/SectionMain'
|
import SectionMain from '../components/SectionMain'
|
||||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
|
||||||
import BaseIcon from "../components/BaseIcon";
|
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import { useAppSelector, useAppDispatch } from '../stores/hooks';
|
||||||
|
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
|
||||||
import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
|
|
||||||
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
|
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
const corners = useAppSelector((state) => state.style.corners);
|
const corners = useAppSelector((state) => state.style.corners);
|
||||||
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
|
||||||
|
|
||||||
const loadingMessage = 'Loading...';
|
|
||||||
|
|
||||||
|
|
||||||
const [users, setUsers] = React.useState(loadingMessage);
|
|
||||||
const [roles, setRoles] = React.useState(loadingMessage);
|
|
||||||
const [permissions, setPermissions] = React.useState(loadingMessage);
|
|
||||||
const [staff_roles, setStaff_roles] = React.useState(loadingMessage);
|
|
||||||
const [customers, setCustomers] = React.useState(loadingMessage);
|
|
||||||
const [tables, setTables] = React.useState(loadingMessage);
|
|
||||||
const [reservations, setReservations] = React.useState(loadingMessage);
|
|
||||||
const [tabs, setTabs] = React.useState(loadingMessage);
|
|
||||||
const [line_items, setLine_items] = React.useState(loadingMessage);
|
|
||||||
const [menu_items, setMenu_items] = React.useState(loadingMessage);
|
|
||||||
const [inventory_items, setInventory_items] = React.useState(loadingMessage);
|
|
||||||
const [suppliers, setSuppliers] = React.useState(loadingMessage);
|
|
||||||
const [payments, setPayments] = React.useState(loadingMessage);
|
|
||||||
const [promotions, setPromotions] = React.useState(loadingMessage);
|
|
||||||
|
|
||||||
|
|
||||||
const [widgetsRole, setWidgetsRole] = React.useState({
|
|
||||||
role: { value: '', label: '' },
|
|
||||||
});
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
|
||||||
|
|
||||||
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
|
||||||
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
const entities = ['users','roles','permissions','staff_roles','customers','tables','reservations','tabs','line_items','menu_items','inventory_items','suppliers','payments','promotions',];
|
|
||||||
const fns = [setUsers,setRoles,setPermissions,setStaff_roles,setCustomers,setTables,setReservations,setTabs,setLine_items,setMenu_items,setInventory_items,setSuppliers,setPayments,setPromotions,];
|
|
||||||
|
|
||||||
const requests = entities.map((entity, index) => {
|
|
||||||
|
|
||||||
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
|
|
||||||
return axios.get(`/${entity.toLowerCase()}/count`);
|
|
||||||
} else {
|
|
||||||
fns[index](null);
|
|
||||||
return Promise.resolve({data: {count: null}});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
Promise.allSettled(requests).then((results) => {
|
|
||||||
results.forEach((result, i) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
fns[i](result.value.data.count);
|
|
||||||
} else {
|
|
||||||
fns[i](result.reason.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getWidgets(roleId) {
|
|
||||||
await dispatch(fetchWidgets(roleId));
|
|
||||||
}
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!currentUser) return;
|
|
||||||
loadData().then();
|
|
||||||
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
|
|
||||||
}, [currentUser]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!currentUser || !widgetsRole?.role?.value) return;
|
|
||||||
getWidgets(widgetsRole?.role?.value || '').then();
|
|
||||||
}, [widgetsRole?.role?.value]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -104,445 +40,6 @@ const Dashboard = () => {
|
|||||||
main>
|
main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
|
||||||
currentUser={currentUser}
|
|
||||||
isFetchingQuery={isFetchingQuery}
|
|
||||||
setWidgetsRole={setWidgetsRole}
|
|
||||||
widgetsRole={widgetsRole}
|
|
||||||
/>}
|
|
||||||
{!!rolesWidgets.length &&
|
|
||||||
hasPermission(currentUser, 'CREATE_ROLES') && (
|
|
||||||
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
|
||||||
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
|
|
||||||
{(isFetchingQuery || loading) && (
|
|
||||||
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor} animate-spin mr-5`}
|
|
||||||
w='w-16'
|
|
||||||
h='h-16'
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiLoading}
|
|
||||||
/>{' '}
|
|
||||||
Loading widgets...
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ rolesWidgets &&
|
|
||||||
rolesWidgets.map((widget) => (
|
|
||||||
<SmartWidget
|
|
||||||
key={widget.id}
|
|
||||||
userId={currentUser?.id}
|
|
||||||
widget={widget}
|
|
||||||
roleId={widgetsRole?.role?.value || ''}
|
|
||||||
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!!rolesWidgets.length && <hr className='my-6 text-midnightBlueTheme-mainBG ' />}
|
|
||||||
|
|
||||||
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
|
||||||
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Users
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{users}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiAccountGroup || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Roles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Permissions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{permissions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={icon.mdiShieldAccountOutline || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_STAFF_ROLES') && <Link href={'/staff_roles/staff_roles-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Staff roles
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{staff_roles}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiShieldAccount' in icon ? icon['mdiShieldAccount' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_CUSTOMERS') && <Link href={'/customers/customers-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Customers
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{customers}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiAccount' in icon ? icon['mdiAccount' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_TABLES') && <Link href={'/tables/tables-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Tables
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{tables}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiSeat' in icon ? icon['mdiSeat' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_RESERVATIONS') && <Link href={'/reservations/reservations-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Reservations
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{reservations}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCalendar' in icon ? icon['mdiCalendar' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_TABS') && <Link href={'/tabs/tabs-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Tabs
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{tabs}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiWallet' in icon ? icon['mdiWallet' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_LINE_ITEMS') && <Link href={'/line_items/line_items-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Line items
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{line_items}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_MENU_ITEMS') && <Link href={'/menu_items/menu_items-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Menu items
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{menu_items}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiFood' in icon ? icon['mdiFood' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_INVENTORY_ITEMS') && <Link href={'/inventory_items/inventory_items-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Inventory items
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{inventory_items}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiWarehouse' in icon ? icon['mdiWarehouse' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_SUPPLIERS') && <Link href={'/suppliers/suppliers-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Suppliers
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{suppliers}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiTruck' in icon ? icon['mdiTruck' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PAYMENTS') && <Link href={'/payments/payments-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Payments
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{payments}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiCreditCard' in icon ? icon['mdiCreditCard' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
{hasPermission(currentUser, 'READ_PROMOTIONS') && <Link href={'/promotions/promotions-list'}>
|
|
||||||
<div
|
|
||||||
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between align-center">
|
|
||||||
<div>
|
|
||||||
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
|
|
||||||
Promotions
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{promotions}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className={`${iconsColor}`}
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
path={'mdiTag' in icon ? icon['mdiTag' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import axios from 'axios';
|
|||||||
interface MainState {
|
interface MainState {
|
||||||
isFetchingQuery: boolean;
|
isFetchingQuery: boolean;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
smartWidgets: any[];
|
|
||||||
gptResponse: string | null;
|
gptResponse: string | null;
|
||||||
aiResponse: any | null;
|
aiResponse: any | null;
|
||||||
isAskingQuestion: boolean;
|
isAskingQuestion: boolean;
|
||||||
@ -18,7 +18,7 @@ interface MainState {
|
|||||||
const initialState: MainState = {
|
const initialState: MainState = {
|
||||||
isFetchingQuery: false,
|
isFetchingQuery: false,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
smartWidgets: [],
|
|
||||||
gptResponse: null,
|
gptResponse: null,
|
||||||
aiResponse: null,
|
aiResponse: null,
|
||||||
isAskingQuestion: false,
|
isAskingQuestion: false,
|
||||||
@ -36,20 +36,6 @@ const fulfilledNotify = (state, msg, type?: string) => {
|
|||||||
state.notify.showNotification = true;
|
state.notify.showNotification = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const aiPrompt = createAsyncThunk(
|
|
||||||
'openai/aiPrompt',
|
|
||||||
async (data: any, { rejectWithValue }) => {
|
|
||||||
try {
|
|
||||||
return await axios.post('/openai/create_widget', data);
|
|
||||||
} catch (error) {
|
|
||||||
if (!error.response) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return rejectWithValue(error.response.data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const askGpt = createAsyncThunk(
|
export const askGpt = createAsyncThunk(
|
||||||
'openai/askGpt',
|
'openai/askGpt',
|
||||||
async (prompt: string, { rejectWithValue }) => {
|
async (prompt: string, { rejectWithValue }) => {
|
||||||
@ -94,21 +80,6 @@ export const openAiSlice = createSlice({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(aiPrompt.pending, (state) => {
|
|
||||||
state.isFetchingQuery = true;
|
|
||||||
});
|
|
||||||
builder.addCase(aiPrompt.fulfilled, (state, action: Record<any, any>) => {
|
|
||||||
state.isFetchingQuery = false;
|
|
||||||
state.errorMessage = '';
|
|
||||||
state.smartWidgets.unshift(action.payload.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.addCase(aiPrompt.rejected, (state) => {
|
|
||||||
state.errorMessage = 'Something was wrong. Try again';
|
|
||||||
state.isFetchingQuery = false;
|
|
||||||
state.smartWidgets = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.addCase(askGpt.pending, (state) => {
|
builder.addCase(askGpt.pending, (state) => {
|
||||||
state.isAskingQuestion = true;
|
state.isAskingQuestion = true;
|
||||||
state.gptResponse = null;
|
state.gptResponse = null;
|
||||||
|
|||||||
@ -7,7 +7,7 @@ interface MainState {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
count: number
|
count: number
|
||||||
refetch: boolean;
|
refetch: boolean;
|
||||||
rolesWidgets: any[];
|
|
||||||
notify: {
|
notify: {
|
||||||
showNotification: boolean
|
showNotification: boolean
|
||||||
textNotification: string
|
textNotification: string
|
||||||
@ -20,7 +20,7 @@ const initialState: MainState = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
count: 0,
|
count: 0,
|
||||||
refetch: false,
|
refetch: false,
|
||||||
rolesWidgets: [],
|
|
||||||
notify: {
|
notify: {
|
||||||
showNotification: false,
|
showNotification: false,
|
||||||
textNotification: '',
|
textNotification: '',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user