feat: add formatRupiahShort function for concise currency formatting and update Analysis.vue to utilize new formatting
This commit is contained in:
parent
6e75debff0
commit
fbd5bd7b6d
@ -11,3 +11,28 @@ export function formatRupiah(value: string | number): string {
|
||||
|
||||
return Number(digits).toLocaleString('id-ID');
|
||||
}
|
||||
|
||||
export function formatRupiahShort(value: number): string {
|
||||
const abs = Math.abs(value);
|
||||
const sign = value < 0 ? '-' : '';
|
||||
|
||||
if (abs >= 1_000_000_000) {
|
||||
const v = abs / 1_000_000_000;
|
||||
|
||||
return sign + (v % 1 === 0 ? v.toFixed(0) : v.toFixed(1).replace('.', ',')) + 'M';
|
||||
}
|
||||
|
||||
if (abs >= 1_000_000) {
|
||||
const v = abs / 1_000_000;
|
||||
|
||||
return sign + (v % 1 === 0 ? v.toFixed(0) : v.toFixed(1).replace('.', ',')) + 'jt';
|
||||
}
|
||||
|
||||
if (abs >= 1_000) {
|
||||
const v = abs / 1_000;
|
||||
|
||||
return sign + (v % 1 === 0 ? v.toFixed(0) : v.toFixed(1).replace('.', ',')) + 'rb';
|
||||
}
|
||||
|
||||
return sign + abs.toString();
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { useCan } from '@/composables/useCan';
|
||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||
import { formatRupiah } from '@/lib/rupiah';
|
||||
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
||||
import admin from '@/routes/admin';
|
||||
|
||||
const { can, hasRole } = useCan();
|
||||
@ -184,8 +184,6 @@ const revenueChartConfig = {
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const activeRevenueChart = ref<'total' | 'net' | 'deduction'>('total');
|
||||
|
||||
const revenueTotals = computed(() => ({
|
||||
total: props.revenueSummary.total_revenue,
|
||||
net: props.revenueSummary.total_revenue - props.revenueSummary.total_deduction,
|
||||
@ -214,8 +212,6 @@ const expenseChartConfig = {
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const activeExpenseChart = ref<'total' | 'purchase' | 'expense' | 'advance'>('total');
|
||||
|
||||
const expenseTotals = computed(() => ({
|
||||
total: props.expenseSummary.total,
|
||||
purchase: props.expenseSummary.purchase_total,
|
||||
@ -233,6 +229,40 @@ const visibleExpenseCharts = computed(() => {
|
||||
return charts;
|
||||
});
|
||||
|
||||
const expenseYAccessors = computed(() => {
|
||||
if (hasRole('admin-toko')) {
|
||||
return [
|
||||
(d: MonthlyExpenseData) => d.total,
|
||||
(d: MonthlyExpenseData) => d.expense,
|
||||
(d: MonthlyExpenseData) => d.advance,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
(d: MonthlyExpenseData) => d.total,
|
||||
(d: MonthlyExpenseData) => d.purchase,
|
||||
(d: MonthlyExpenseData) => d.expense,
|
||||
(d: MonthlyExpenseData) => d.advance,
|
||||
];
|
||||
});
|
||||
|
||||
const expenseColors = computed(() => {
|
||||
if (hasRole('admin-toko')) {
|
||||
return [
|
||||
expenseChartConfig.total.color,
|
||||
expenseChartConfig.expense.color,
|
||||
expenseChartConfig.advance.color,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
expenseChartConfig.total.color,
|
||||
expenseChartConfig.purchase.color,
|
||||
expenseChartConfig.expense.color,
|
||||
expenseChartConfig.advance.color,
|
||||
];
|
||||
});
|
||||
|
||||
// Busy Hours Chart
|
||||
type BusyHourData = { hour: string; orders: number };
|
||||
|
||||
@ -283,26 +313,40 @@ const barSelector = GroupedBar.selectors.bar;
|
||||
function revenueTooltip(d: any) {
|
||||
const item = d as MonthlyRevenueData;
|
||||
|
||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2 mt-0!">
|
||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.6">
|
||||
<p style="margin:0;font-weight:500">${item.month}</p>
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig[activeRevenueChart.value].color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig[activeRevenueChart.value].label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item[activeRevenueChart.value])}</span>
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig.total.color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig.total.label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.total)}</span>
|
||||
</p>
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig.net.color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig.net.label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.net)}</span>
|
||||
</p>
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${revenueChartConfig.deduction.color}"></span>
|
||||
<span class="text-muted-foreground">${revenueChartConfig.deduction.label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item.deduction)}</span>
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function expenseTooltip(d: any) {
|
||||
const item = d as MonthlyExpenseData;
|
||||
const charts = visibleExpenseCharts.value;
|
||||
const rows = charts.map((chart) =>
|
||||
`<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${expenseChartConfig[chart].color}"></span>
|
||||
<span class="text-muted-foreground">${expenseChartConfig[chart].label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item[chart])}</span>
|
||||
</p>`
|
||||
).join('');
|
||||
|
||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.2">
|
||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.6">
|
||||
<p style="margin:0;font-weight:500">${item.month}</p>
|
||||
<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
||||
<span class="size-2.5 rounded-full" style="background-color: ${expenseChartConfig[activeExpenseChart.value].color}"></span>
|
||||
<span class="text-muted-foreground">${expenseChartConfig[activeExpenseChart.value].label}</span>
|
||||
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item[activeExpenseChart.value])}</span>
|
||||
</p>
|
||||
${rows}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@ -537,36 +581,48 @@ watch([startDate, endDate], () => {
|
||||
<CardTitle>Pendapatan</CardTitle>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button v-for="chart in ['total', 'net', 'deduction'] as const" :key="chart"
|
||||
:data-active="activeRevenueChart === chart"
|
||||
class="data-[active=true]:bg-muted/50 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6"
|
||||
@click="activeRevenueChart = chart">
|
||||
<div v-for="chart in ['total', 'net', 'deduction'] as const" :key="chart"
|
||||
class="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6">
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ revenueChartConfig[chart].label }}
|
||||
</span>
|
||||
<span class="text-sm">
|
||||
Rp{{ formatRupiah(revenueTotals[chart]) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="px-2 sm:p-6">
|
||||
<ChartContainer v-if="monthlyRevenue.length > 0" :config="revenueChartConfig"
|
||||
class="aspect-auto h-[300px] w-full">
|
||||
<VisXYContainer :data="monthlyRevenue" :y-domain="[0, undefined]">
|
||||
<VisGroupedBar :x="(_d: MonthlyRevenueData, i: number) => i"
|
||||
:y="(d: MonthlyRevenueData) => d[activeRevenueChart]"
|
||||
:color="revenueChartConfig[activeRevenueChart].color" :bar-padding="0.1"
|
||||
:rounded-corners="4" />
|
||||
<VisAxis type="x" :x="(_d: MonthlyRevenueData, i: number) => i" :tick-line="false"
|
||||
:domain-line="false" :grid-line="false"
|
||||
:tick-format="(d: number) => monthlyRevenue[d]?.month ?? ''"
|
||||
:tick-values="monthlyRevenue.map((_, i) => i)" />
|
||||
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
|
||||
<VisTooltip :triggers="{ [barSelector]: revenueTooltip }" class-name="custom-tooltip" />
|
||||
</VisXYContainer>
|
||||
</ChartContainer>
|
||||
<div v-if="monthlyRevenue.length > 0">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-center gap-4">
|
||||
<div v-for="chart in ['total', 'net', 'deduction'] as const" :key="chart"
|
||||
class="flex items-center gap-1.5">
|
||||
<span class="size-2.5 rounded-full"
|
||||
:style="{ backgroundColor: revenueChartConfig[chart].color }"></span>
|
||||
<span class="text-xs text-muted-foreground">{{ revenueChartConfig[chart].label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChartContainer :config="revenueChartConfig" class="aspect-auto h-[300px] w-full">
|
||||
<VisXYContainer :data="monthlyRevenue" :y-domain="[0, undefined]">
|
||||
<VisGroupedBar :x="(_d: MonthlyRevenueData, i: number) => i" :y="[
|
||||
(d: MonthlyRevenueData) => d.total,
|
||||
(d: MonthlyRevenueData) => d.net,
|
||||
(d: MonthlyRevenueData) => d.deduction,
|
||||
]" :color="[
|
||||
revenueChartConfig.total.color,
|
||||
revenueChartConfig.net.color,
|
||||
revenueChartConfig.deduction.color,
|
||||
]" :bar-padding="0.1" :group-padding="0.2" :rounded-corners="4" />
|
||||
<VisAxis type="x" :x="(_d: MonthlyRevenueData, i: number) => i" :tick-line="false"
|
||||
:domain-line="false" :grid-line="false"
|
||||
:tick-format="(d: number) => monthlyRevenue[d]?.month ?? ''"
|
||||
:tick-values="monthlyRevenue.map((_, i) => i)" />
|
||||
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiahShort(d)" />
|
||||
<VisTooltip :triggers="{ [barSelector]: revenueTooltip }" class-name="custom-tooltip" />
|
||||
</VisXYContainer>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<div v-else class="flex h-[300px] items-center justify-center text-muted-foreground">
|
||||
Belum ada data pendapatan
|
||||
</div>
|
||||
@ -580,36 +636,41 @@ watch([startDate, endDate], () => {
|
||||
<CardTitle>Pengeluaran</CardTitle>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button v-for="chart in visibleExpenseCharts" :key="chart"
|
||||
:data-active="activeExpenseChart === chart"
|
||||
class="data-[active=true]:bg-muted/50 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6"
|
||||
@click="activeExpenseChart = chart">
|
||||
<div v-for="chart in visibleExpenseCharts" :key="chart"
|
||||
class="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l sm:border-t-0 sm:border-l sm:px-8 sm:py-6">
|
||||
<span class="text-muted-foreground text-xs">
|
||||
{{ expenseChartConfig[chart].label }}
|
||||
</span>
|
||||
<span class="text-sm">
|
||||
Rp{{ formatRupiah(expenseTotals[chart]) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent class="px-2 sm:p-6">
|
||||
<ChartContainer v-if="monthlyExpense.length > 0" :config="expenseChartConfig"
|
||||
class="aspect-auto h-[300px] w-full">
|
||||
<VisXYContainer :data="monthlyExpense" :y-domain="[0, undefined]">
|
||||
<VisGroupedBar :x="(_d: MonthlyExpenseData, i: number) => i"
|
||||
:y="(d: MonthlyExpenseData) => d[activeExpenseChart]"
|
||||
:color="expenseChartConfig[activeExpenseChart].color" :bar-padding="0.1"
|
||||
:rounded-corners="4" />
|
||||
<VisAxis type="x" :x="(_d: MonthlyExpenseData, i: number) => i" :tick-line="false"
|
||||
:domain-line="false" :grid-line="false"
|
||||
:tick-format="(d: number) => monthlyExpense[d]?.month ?? ''"
|
||||
:tick-values="monthlyExpense.map((_, i) => i)" />
|
||||
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
|
||||
<VisTooltip :triggers="{ [barSelector]: expenseTooltip }" class-name="custom-tooltip" />
|
||||
</VisXYContainer>
|
||||
</ChartContainer>
|
||||
<div v-if="monthlyExpense.length > 0">
|
||||
<div class="mb-3 flex flex-wrap items-center justify-center gap-4">
|
||||
<div v-for="chart in visibleExpenseCharts" :key="chart" class="flex items-center gap-1.5">
|
||||
<span class="size-2.5 rounded-full"
|
||||
:style="{ backgroundColor: expenseChartConfig[chart].color }"></span>
|
||||
<span class="text-xs text-muted-foreground">{{ expenseChartConfig[chart].label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChartContainer :config="expenseChartConfig" class="aspect-auto h-[300px] w-full">
|
||||
<VisXYContainer :data="monthlyExpense" :y-domain="[0, undefined]">
|
||||
<VisGroupedBar :x="(_d: MonthlyExpenseData, i: number) => i" :y="expenseYAccessors"
|
||||
:color="expenseColors" :bar-padding="0.1" :group-padding="0.2"
|
||||
:rounded-corners="4" />
|
||||
<VisAxis type="x" :x="(_d: MonthlyExpenseData, i: number) => i" :tick-line="false"
|
||||
:domain-line="false" :grid-line="false"
|
||||
:tick-format="(d: number) => monthlyExpense[d]?.month ?? ''"
|
||||
:tick-values="monthlyExpense.map((_, i) => i)" />
|
||||
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiahShort(d)" />
|
||||
<VisTooltip :triggers="{ [barSelector]: expenseTooltip }" class-name="custom-tooltip" />
|
||||
</VisXYContainer>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
<div v-else class="flex h-[300px] items-center justify-center text-muted-foreground">
|
||||
Belum ada data pengeluaran
|
||||
</div>
|
||||
@ -622,7 +683,6 @@ watch([startDate, endDate], () => {
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Jam Sibuk Toko</CardTitle>
|
||||
<CardDescription>Distribusi pesanan berdasarkan jam dalam sehari</CardDescription>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm text-muted-foreground">Jam Tersibuk</p>
|
||||
@ -714,7 +774,7 @@ watch([startDate, endDate], () => {
|
||||
:tick-format="(d: number) => supplierBarData[d]?.name ?? ''"
|
||||
:tick-values="supplierBarData.map((_, i) => i)" />
|
||||
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false"
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiahShort(d)" />
|
||||
<VisTooltip :triggers="{ [barSelector]: supplierTooltip }"
|
||||
class-name="custom-tooltip" />
|
||||
</VisXYContainer>
|
||||
@ -742,7 +802,7 @@ watch([startDate, endDate], () => {
|
||||
:tick-format="(d: number) => customerBarData[d]?.name ?? ''"
|
||||
:tick-values="customerBarData.map((_, i) => i)" />
|
||||
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false"
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiah(d)" />
|
||||
:tick-format="(d: number) => 'Rp' + formatRupiahShort(d)" />
|
||||
<VisTooltip :triggers="{ [barSelector]: customerTooltip }"
|
||||
class-name="custom-tooltip" />
|
||||
</VisXYContainer>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user