feat: implement dynamic color generation for charts and improve color usage in analysis and dashboard components
This commit is contained in:
parent
ec5da5c5ba
commit
502f2fdf77
@ -85,12 +85,12 @@ :root {
|
|||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.708 0 0);
|
||||||
--chart-1: oklch(0.879 0.169 91.605);
|
--chart-1: oklch(0.65 0.15 250);
|
||||||
--chart-2: oklch(0.769 0.188 70.08);
|
--chart-2: oklch(0.72 0.17 145);
|
||||||
--chart-3: oklch(0.666 0.179 58.318);
|
--chart-3: oklch(0.75 0.18 70);
|
||||||
--chart-4: oklch(0.555 0.163 48.998);
|
--chart-4: oklch(0.60 0.18 300);
|
||||||
--chart-5: oklch(0.473 0.137 46.201);
|
--chart-5: oklch(0.65 0.20 25);
|
||||||
--chart-6: oklch(0.696 0.17 162.48);
|
--chart-6: oklch(0.75 0.12 195);
|
||||||
--radius: 0.625rem;
|
--radius: 0.625rem;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--sidebar: oklch(0.985 0 0);
|
||||||
--sidebar-foreground: oklch(0.145 0 0);
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
@ -122,12 +122,12 @@ .dark {
|
|||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
--chart-1: oklch(0.879 0.169 91.605);
|
--chart-1: oklch(0.70 0.15 250);
|
||||||
--chart-2: oklch(0.769 0.188 70.08);
|
--chart-2: oklch(0.77 0.17 145);
|
||||||
--chart-3: oklch(0.666 0.179 58.318);
|
--chart-3: oklch(0.80 0.18 70);
|
||||||
--chart-4: oklch(0.555 0.163 48.998);
|
--chart-4: oklch(0.65 0.18 300);
|
||||||
--chart-5: oklch(0.473 0.137 46.201);
|
--chart-5: oklch(0.70 0.20 25);
|
||||||
--chart-6: oklch(0.696 0.17 162.48);
|
--chart-6: oklch(0.80 0.12 195);
|
||||||
--sidebar: oklch(0.205 0 0);
|
--sidebar: oklch(0.205 0 0);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
--sidebar-primary: oklch(0.769 0.188 70.08);
|
--sidebar-primary: oklch(0.769 0.188 70.08);
|
||||||
|
|||||||
@ -18,3 +18,14 @@ export function formatCurrency(amount: number): string {
|
|||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function generateRandomColors(count: number): string[] {
|
||||||
|
const colors: string[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const hue = Math.floor(Math.random() * 360);
|
||||||
|
const saturation = 65 + Math.floor(Math.random() * 20);
|
||||||
|
const lightness = 45 + Math.floor(Math.random() * 20);
|
||||||
|
colors.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`);
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { useCan } from '@/hooks/use-can';
|
import { useCan } from '@/hooks/use-can';
|
||||||
|
import { generateRandomColors } from '@/lib/utils';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { formatRupiah } from '@/lib/rupiah';
|
||||||
import { Head, router } from '@inertiajs/react';
|
import { Head, router } from '@inertiajs/react';
|
||||||
import {
|
import {
|
||||||
@ -198,85 +199,59 @@ type AnalysisProps = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const revenueChartConfig = {
|
const revenueChartConfig = (() => {
|
||||||
total: {
|
const colors = generateRandomColors(6);
|
||||||
label: 'Total',
|
return {
|
||||||
color: 'var(--chart-1)',
|
total: { label: 'Total', color: colors[0] },
|
||||||
},
|
gross: { label: 'Keuntungan Kotor', color: colors[1] },
|
||||||
gross: {
|
net: { label: 'Keuntungan Bersih', color: colors[2] },
|
||||||
label: 'Keuntungan Kotor',
|
deduction: { label: 'Potongan Nego', color: colors[3] },
|
||||||
color: 'var(--chart-2)',
|
discount: { label: 'Diskon', color: colors[4] },
|
||||||
},
|
cogs: { label: 'HPP', color: colors[5] },
|
||||||
net: {
|
} satisfies ChartConfig;
|
||||||
label: 'Keuntungan Bersih',
|
})();
|
||||||
color: 'var(--chart-6)',
|
|
||||||
},
|
|
||||||
deduction: {
|
|
||||||
label: 'Potongan Nego',
|
|
||||||
color: 'var(--chart-3)',
|
|
||||||
},
|
|
||||||
discount: {
|
|
||||||
label: 'Diskon',
|
|
||||||
color: 'var(--chart-4)',
|
|
||||||
},
|
|
||||||
cogs: {
|
|
||||||
label: 'HPP',
|
|
||||||
color: 'var(--chart-5)',
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const;
|
const revenueKeys = ['total', 'deduction', 'discount', 'cogs', 'gross', 'net'] as const;
|
||||||
|
|
||||||
const expenseChartConfig = {
|
const expenseChartConfig = (() => {
|
||||||
total: {
|
const colors = generateRandomColors(4);
|
||||||
label: 'Total',
|
return {
|
||||||
color: 'var(--chart-1)',
|
total: { label: 'Total', color: colors[0] },
|
||||||
},
|
purchase: { label: 'Belanja', color: colors[1] },
|
||||||
purchase: {
|
expense: { label: 'Pengeluaran Toko', color: colors[2] },
|
||||||
label: 'Belanja',
|
advance: { label: 'Kasbon', color: colors[3] },
|
||||||
color: 'var(--chart-2)',
|
} satisfies ChartConfig;
|
||||||
},
|
})();
|
||||||
expense: {
|
|
||||||
label: 'Pengeluaran Toko',
|
|
||||||
color: 'var(--chart-3)',
|
|
||||||
},
|
|
||||||
advance: {
|
|
||||||
label: 'Kasbon',
|
|
||||||
color: 'var(--chart-4)',
|
|
||||||
},
|
|
||||||
} satisfies ChartConfig;
|
|
||||||
|
|
||||||
const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const;
|
const expenseKeys = ['total', 'purchase', 'expense', 'advance'] as const;
|
||||||
|
|
||||||
const revenueTrendChartConfig = {
|
const revenueTrendChartConfig = (() => {
|
||||||
qty: {
|
const colors = generateRandomColors(1);
|
||||||
label: 'Qty',
|
return {
|
||||||
color: 'var(--chart-1)',
|
qty: { label: 'Qty', color: colors[0] },
|
||||||
},
|
} satisfies ChartConfig;
|
||||||
} satisfies ChartConfig;
|
})();
|
||||||
|
|
||||||
const revenueTrendKeys = ['qty'] as const;
|
const revenueTrendKeys = ['qty'] as const;
|
||||||
|
|
||||||
const CHANNEL_COLORS: Record<string, string> = {
|
const CHANNEL_COLORS: Record<string, string> = (() => {
|
||||||
store: '#22c55e',
|
const colors = generateRandomColors(3);
|
||||||
shopee: '#ee4d2d',
|
return {
|
||||||
tiktok: '#000000',
|
store: colors[0],
|
||||||
};
|
shopee: colors[1],
|
||||||
|
tiktok: colors[2],
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
const PAYMENT_COLORS: Record<string, string> = {
|
const PAYMENT_COLORS: Record<string, string> = (() => {
|
||||||
cash: '#22c55e',
|
const colors = generateRandomColors(4);
|
||||||
transfer: '#60a5fa',
|
return {
|
||||||
qris: '#a855f7',
|
cash: colors[0],
|
||||||
marketplace: '#f97316',
|
transfer: colors[1],
|
||||||
};
|
qris: colors[2],
|
||||||
|
marketplace: colors[3],
|
||||||
const PIE_COLORS = [
|
};
|
||||||
'var(--chart-1)',
|
})();
|
||||||
'var(--chart-2)',
|
|
||||||
'var(--chart-3)',
|
|
||||||
'var(--chart-4)',
|
|
||||||
'var(--chart-5)',
|
|
||||||
];
|
|
||||||
|
|
||||||
type PieChartItem = {
|
type PieChartItem = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -293,23 +268,25 @@ type DashboardPieChartProps = {
|
|||||||
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
|
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
|
||||||
const hasData = data.length > 0 && data.some((d) => d.count > 0);
|
const hasData = data.length > 0 && data.some((d) => d.count > 0);
|
||||||
|
|
||||||
|
const pieColors = useMemo(() => generateRandomColors(5), []);
|
||||||
|
|
||||||
const chartConfig = useMemo(() => {
|
const chartConfig = useMemo(() => {
|
||||||
const config: ChartConfig = {};
|
const config: ChartConfig = {};
|
||||||
data.forEach((item, index) => {
|
data.forEach((item, index) => {
|
||||||
config[item.name] = {
|
config[item.name] = {
|
||||||
label: item.name,
|
label: item.name,
|
||||||
color: PIE_COLORS[index % PIE_COLORS.length],
|
color: pieColors[index % pieColors.length],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return config;
|
return config;
|
||||||
}, [data]);
|
}, [data, pieColors]);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length],
|
fill: pieColors[data.indexOf(item) % pieColors.length],
|
||||||
}));
|
}));
|
||||||
}, [data]);
|
}, [data, pieColors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -1042,7 +1019,7 @@ export default function Analysis({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 sm:p-6">
|
<CardContent className="px-2 sm:p-6">
|
||||||
{topSuppliers.length > 0 ? (
|
{topSuppliers.length > 0 ? (
|
||||||
<ChartContainer config={{ amount: { label: 'Total Pembelian', color: 'var(--chart-1)' } }} className="aspect-auto h-[250px] w-full">
|
<ChartContainer config={{ amount: { label: 'Total Pembelian', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
|
||||||
<BarChart data={topSuppliers.map((s) => ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))} margin={{ left: 12, right: 12 }}>
|
<BarChart data={topSuppliers.map((s) => ({ name: s.name.length > 15 ? s.name.substring(0, 15) + '...' : s.name, amount: s.total_amount }))} margin={{ left: 12, right: 12 }}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
|
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
|
||||||
@ -1084,7 +1061,7 @@ export default function Analysis({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 sm:p-6">
|
<CardContent className="px-2 sm:p-6">
|
||||||
{topProducts.length > 0 ? (
|
{topProducts.length > 0 ? (
|
||||||
<ChartContainer config={{ qty: { label: 'Jumlah Terjual', color: 'var(--chart-3)' } }} className="aspect-auto h-[250px] w-full">
|
<ChartContainer config={{ qty: { label: 'Jumlah Terjual', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
|
||||||
<BarChart data={topProducts.map((p) => ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))} margin={{ left: 12, right: 12 }}>
|
<BarChart data={topProducts.map((p) => ({ name: p.name.length > 20 ? p.name.substring(0, 20) + '...' : p.name, qty: p.total_qty }))} margin={{ left: 12, right: 12 }}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
|
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
|
||||||
@ -1126,7 +1103,7 @@ export default function Analysis({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 sm:p-6">
|
<CardContent className="px-2 sm:p-6">
|
||||||
{topCustomers.length > 0 ? (
|
{topCustomers.length > 0 ? (
|
||||||
<ChartContainer config={{ amount: { label: 'Total Pesanan', color: 'var(--chart-2)' } }} className="aspect-auto h-[250px] w-full">
|
<ChartContainer config={{ amount: { label: 'Total Pesanan', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
|
||||||
<BarChart data={topCustomers.map((c) => ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))} margin={{ left: 12, right: 12 }}>
|
<BarChart data={topCustomers.map((c) => ({ name: c.name.length > 15 ? c.name.substring(0, 15) + '...' : c.name, amount: c.total_amount }))} margin={{ left: 12, right: 12 }}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
|
<XAxis dataKey="name" tickLine={false} axisLine={false} tickMargin={8} />
|
||||||
@ -1180,7 +1157,7 @@ export default function Analysis({
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="px-2 sm:p-6">
|
<CardContent className="px-2 sm:p-6">
|
||||||
{busyHours.length > 0 ? (
|
{busyHours.length > 0 ? (
|
||||||
<ChartContainer config={{ orders: { label: 'Pesanan', color: 'var(--chart-1)' } }} className="aspect-auto h-[250px] w-full">
|
<ChartContainer config={{ orders: { label: 'Pesanan', color: generateRandomColors(1)[0] } }} className="aspect-auto h-[250px] w-full">
|
||||||
<BarChart data={busyHours} margin={{ left: 12, right: 12 }}>
|
<BarChart data={busyHours} margin={{ left: 12, right: 12 }}>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
<XAxis dataKey="hour" tickLine={false} axisLine={false} tickMargin={8} />
|
<XAxis dataKey="hour" tickLine={false} axisLine={false} tickMargin={8} />
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { useCan } from '@/hooks/use-can';
|
import { useCan } from '@/hooks/use-can';
|
||||||
|
import { generateRandomColors } from '@/lib/utils';
|
||||||
import { formatRupiah } from '@/lib/rupiah';
|
import { formatRupiah } from '@/lib/rupiah';
|
||||||
import { store, update } from '@/routes/admin/hr/attendances';
|
import { store, update } from '@/routes/admin/hr/attendances';
|
||||||
import { Head, router, usePage } from '@inertiajs/react';
|
import { Head, router, usePage } from '@inertiajs/react';
|
||||||
@ -467,34 +468,28 @@ type DashboardPieChartProps = {
|
|||||||
nameKey: string;
|
nameKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PIE_COLORS = [
|
|
||||||
'var(--chart-1)',
|
|
||||||
'var(--chart-2)',
|
|
||||||
'var(--chart-3)',
|
|
||||||
'var(--chart-4)',
|
|
||||||
'var(--chart-5)',
|
|
||||||
];
|
|
||||||
|
|
||||||
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
|
function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) {
|
||||||
const hasData = data.length > 0 && data.some((d) => d.count > 0);
|
const hasData = data.length > 0 && data.some((d) => d.count > 0);
|
||||||
|
|
||||||
|
const pieColors = useMemo(() => generateRandomColors(5), []);
|
||||||
|
|
||||||
const chartConfig = useMemo(() => {
|
const chartConfig = useMemo(() => {
|
||||||
const config: ChartConfig = {};
|
const config: ChartConfig = {};
|
||||||
data.forEach((item, index) => {
|
data.forEach((item, index) => {
|
||||||
config[item.name] = {
|
config[item.name] = {
|
||||||
label: item.name,
|
label: item.name,
|
||||||
color: PIE_COLORS[index % PIE_COLORS.length],
|
color: pieColors[index % pieColors.length],
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return config;
|
return config;
|
||||||
}, [data]);
|
}, [data, pieColors]);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
return data.map((item) => ({
|
return data.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length],
|
fill: pieColors[data.indexOf(item) % pieColors.length],
|
||||||
}));
|
}));
|
||||||
}, [data]);
|
}, [data, pieColors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user