feat: enhance permissions and layout in Analysis component for better role-based display
This commit is contained in:
parent
b7f8bf7e0f
commit
1e8fd5b595
@ -132,7 +132,8 @@ public function permissions(): array
|
|||||||
Permission::ANALYSIS_EXPENSE,
|
Permission::ANALYSIS_EXPENSE,
|
||||||
Permission::ANALYSIS_BUSY_HOURS,
|
Permission::ANALYSIS_BUSY_HOURS,
|
||||||
Permission::ANALYSIS_PROFIT_ORDERS,
|
Permission::ANALYSIS_PROFIT_ORDERS,
|
||||||
Permission::ANALYSIS_TOP_SUPPLIERS,
|
Permission::ANALYSIS_PROFIT_HPP,
|
||||||
|
Permission::ANALYSIS_PROFIT_GROSS,
|
||||||
Permission::ANALYSIS_TOP_CUSTOMERS,
|
Permission::ANALYSIS_TOP_CUSTOMERS,
|
||||||
Permission::ANALYSIS_TOP_PRODUCTS,
|
Permission::ANALYSIS_TOP_PRODUCTS,
|
||||||
Permission::ANALYSIS_MARKETING_SALES,
|
Permission::ANALYSIS_MARKETING_SALES,
|
||||||
@ -332,6 +333,7 @@ public function permissions(): array
|
|||||||
Permission::ANALYSIS_REVENUE,
|
Permission::ANALYSIS_REVENUE,
|
||||||
Permission::ANALYSIS_PROFIT_ORDERS,
|
Permission::ANALYSIS_PROFIT_ORDERS,
|
||||||
Permission::ANALYSIS_TOP_PRODUCTS,
|
Permission::ANALYSIS_TOP_PRODUCTS,
|
||||||
|
Permission::ANALYSIS_TOP_CUSTOMERS,
|
||||||
|
|
||||||
Permission::ATTENDANCES_VIEW,
|
Permission::ATTENDANCES_VIEW,
|
||||||
Permission::ATTENDANCES_CREATE,
|
Permission::ATTENDANCES_CREATE,
|
||||||
|
|||||||
@ -382,6 +382,49 @@ const busyHourTickValues = computed(() => {
|
|||||||
return all;
|
return all;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sectionOrder = computed(() => {
|
||||||
|
if (hasAnyRole(['owner', 'developer'])) {
|
||||||
|
return {
|
||||||
|
statCards: 1,
|
||||||
|
revenue: 5,
|
||||||
|
expense: 6,
|
||||||
|
profitGross: 7,
|
||||||
|
totalOrder: 8,
|
||||||
|
marketingSales: 9,
|
||||||
|
topSuppliers: 10,
|
||||||
|
topProducts: 11,
|
||||||
|
topCustomers: 12,
|
||||||
|
busyHours: 13,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAnyRole(['admin_toko', 'direktur'])) {
|
||||||
|
return {
|
||||||
|
statCards: 1,
|
||||||
|
revenue: 4,
|
||||||
|
expense: 5,
|
||||||
|
profitGross: 6,
|
||||||
|
totalOrder: 7,
|
||||||
|
marketingSales: 8,
|
||||||
|
topProducts: 9,
|
||||||
|
topCustomers: 10,
|
||||||
|
busyHours: 11,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasRole('marketing')) {
|
||||||
|
return {
|
||||||
|
statCards: 1,
|
||||||
|
revenue: 2,
|
||||||
|
totalOrder: 3,
|
||||||
|
topProducts: 4,
|
||||||
|
topCustomers: 5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
// Top 5 Chart configs
|
// Top 5 Chart configs
|
||||||
type SupplierData = { name: string; amount: number };
|
type SupplierData = { name: string; amount: number };
|
||||||
type CustomerData = { name: string; amount: number };
|
type CustomerData = { name: string; amount: number };
|
||||||
@ -609,8 +652,8 @@ watch([startDate, endDate], () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kehadiran & Kas Toko -->
|
<!-- Stat Cards -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4" :style="{ order: sectionOrder.statCards ?? 99 }">
|
||||||
<StatCard v-if="can('analysis.attendance') && isManager" title="Kehadiran" :icon="UserCheck"
|
<StatCard v-if="can('analysis.attendance') && isManager" title="Kehadiran" :icon="UserCheck"
|
||||||
main-label="Total Karyawan" :main-value="attendance.total_employees"
|
main-label="Total Karyawan" :main-value="attendance.total_employees"
|
||||||
:sub-label="attendance.percentage + '% hadir'" :items="[
|
:sub-label="attendance.percentage + '% hadir'" :items="[
|
||||||
@ -720,7 +763,7 @@ watch([startDate, endDate], () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Revenue Chart -->
|
<!-- Revenue Chart -->
|
||||||
<Card v-if="can('analysis.revenue')" class="py-4 sm:py-0">
|
<Card v-if="can('analysis.revenue')" class="py-4 sm:py-0" :style="{ order: sectionOrder.revenue ?? 99 }">
|
||||||
<CardHeader class="flex flex-col items-stretch border-b p-0! sm:flex-row">
|
<CardHeader class="flex flex-col items-stretch border-b p-0! sm:flex-row">
|
||||||
<div class="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
<div class="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
||||||
<CardTitle>Pendapatan</CardTitle>
|
<CardTitle>Pendapatan</CardTitle>
|
||||||
@ -775,7 +818,7 @@ watch([startDate, endDate], () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Expense Chart -->
|
<!-- Expense Chart -->
|
||||||
<Card v-if="can('analysis.expense')" class="py-4 sm:py-0">
|
<Card v-if="can('analysis.expense')" class="py-4 sm:py-0" :style="{ order: sectionOrder.expense ?? 99 }">
|
||||||
<CardHeader class="flex flex-col items-stretch border-b p-0! sm:flex-row">
|
<CardHeader class="flex flex-col items-stretch border-b p-0! sm:flex-row">
|
||||||
<div class="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
<div class="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
||||||
<CardTitle>Pengeluaran</CardTitle>
|
<CardTitle>Pengeluaran</CardTitle>
|
||||||
@ -830,191 +873,57 @@ watch([startDate, endDate], () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Busy Hours Chart -->
|
<!-- Laba Kotor -->
|
||||||
<Card v-if="can('analysis.busy_hours')">
|
<StatCard v-if="
|
||||||
<CardHeader>
|
can('analysis.profit_gross') ||
|
||||||
<div class="flex items-center justify-between">
|
can('analysis.profit_hpp')
|
||||||
<div>
|
" title="Laba Kotor" :icon="TrendingUp" main-label="Laba Kotor" :main-value="'Rp' + formatRupiah(profitMetrics.gross_profit)
|
||||||
<CardTitle>Jam Sibuk Toko</CardTitle>
|
" :items="[
|
||||||
</div>
|
{
|
||||||
<div class="text-right">
|
label: 'Pendapatan',
|
||||||
<p class="text-sm text-muted-foreground">
|
value:
|
||||||
Jam Tersibuk
|
'Rp' +
|
||||||
</p>
|
formatRupiah(revenueSummary.total_revenue),
|
||||||
<p class="text-2xl font-bold text-primary">
|
},
|
||||||
{{ peakHour.hour }}
|
{
|
||||||
</p>
|
label: 'HPP',
|
||||||
<p class="text-xs text-muted-foreground">
|
value: 'Rp' + formatRupiah(profitMetrics.hpp),
|
||||||
{{ peakHour.orders }} pesanan
|
},
|
||||||
</p>
|
...(!hasRole('cashier') ? [
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer v-if="busyHours.length > 0" :config="busyHoursChartConfig"
|
|
||||||
class="aspect-auto h-[250px] w-full">
|
|
||||||
<VisXYContainer :data="busyHours" :y-domain="[0, undefined]">
|
|
||||||
<VisGroupedBar :x="(_d: BusyHourData, i: number) => i" :y="(d: BusyHourData) => d.orders"
|
|
||||||
:color="busyHoursChartConfig.orders.color" :bar-padding="0.1" :rounded-corners="4" />
|
|
||||||
<VisAxis type="x" :x="(_d: BusyHourData, i: number) => i" :tick-line="false"
|
|
||||||
:domain-line="false" :grid-line="false" :tick-format="(d: number) => busyHours[d]?.hour ?? ''
|
|
||||||
" :tick-values="busyHourTickValues" />
|
|
||||||
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false" :tick-format="(d: number) => Math.round(d).toString()
|
|
||||||
" />
|
|
||||||
<VisTooltip :triggers="{ [barSelector]: busyHoursTooltip }" class-name="custom-tooltip" />
|
|
||||||
</VisXYContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
|
||||||
Belum ada data pesanan
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<!-- Profit Metrics Cards -->
|
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<StatCard v-if="can('analysis.profit_orders')" title="Total Order" :icon="ShoppingCart"
|
|
||||||
main-label="Pesanan Selesai" :main-value="profitMetrics.total_orders.toLocaleString('id-ID')
|
|
||||||
" :items="[
|
|
||||||
{
|
{
|
||||||
label: 'Produk Terjual',
|
label: 'Laba Bersih',
|
||||||
value: profitMetrics.total_products_sold.toLocaleString(
|
|
||||||
'id-ID',
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Item/Transaksi',
|
|
||||||
value: profitMetrics.items_per_transaction,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'AOV',
|
|
||||||
value: 'Rp' + formatRupiah(profitMetrics.aov),
|
|
||||||
},
|
|
||||||
]" />
|
|
||||||
|
|
||||||
<StatCard v-if="
|
|
||||||
can('analysis.profit_gross') ||
|
|
||||||
can('analysis.profit_hpp')
|
|
||||||
" title="Laba Kotor" :icon="TrendingUp" main-label="Laba Kotor" :main-value="'Rp' + formatRupiah(profitMetrics.gross_profit)
|
|
||||||
" :items="[
|
|
||||||
{
|
|
||||||
label: 'Pendapatan',
|
|
||||||
value:
|
value:
|
||||||
'Rp' +
|
'Rp' +
|
||||||
formatRupiah(revenueSummary.total_revenue),
|
formatRupiah(profitMetrics.net_profit) +
|
||||||
|
' (' +
|
||||||
|
profitMetrics.profit_margin +
|
||||||
|
'%)',
|
||||||
},
|
},
|
||||||
{
|
] : []),
|
||||||
label: 'HPP',
|
]" :style="{ order: sectionOrder.profitGross ?? 99 }" />
|
||||||
value: 'Rp' + formatRupiah(profitMetrics.hpp),
|
|
||||||
},
|
|
||||||
...(!hasRole('cashier') ? [
|
|
||||||
{
|
|
||||||
label: 'Laba Bersih',
|
|
||||||
value:
|
|
||||||
'Rp' +
|
|
||||||
formatRupiah(profitMetrics.net_profit) +
|
|
||||||
' (' +
|
|
||||||
profitMetrics.profit_margin +
|
|
||||||
'%)',
|
|
||||||
},
|
|
||||||
] : []),
|
|
||||||
]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Top 5 Charts -->
|
<!-- Total Order -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<StatCard v-if="can('analysis.profit_orders')" title="Total Order" :icon="ShoppingCart"
|
||||||
<Card v-if="can('analysis.top_suppliers')">
|
main-label="Pesanan Selesai" :main-value="profitMetrics.total_orders.toLocaleString('id-ID')
|
||||||
<CardHeader>
|
" :items="[
|
||||||
<CardTitle class="text-base">Top 5 Supplier</CardTitle>
|
{
|
||||||
<CardDescription>Berdasarkan total harga pembelian</CardDescription>
|
label: 'Produk Terjual',
|
||||||
</CardHeader>
|
value: profitMetrics.total_products_sold.toLocaleString(
|
||||||
<CardContent>
|
'id-ID',
|
||||||
<ChartContainer v-if="supplierBarData.length > 0" :config="supplierChartConfig"
|
),
|
||||||
class="aspect-auto h-[250px] w-full">
|
},
|
||||||
<VisXYContainer :data="supplierBarData" :y-domain="[0, undefined]">
|
{
|
||||||
<VisGroupedBar :x="(_d: SupplierData, i: number) => i"
|
label: 'Item/Transaksi',
|
||||||
:y="(d: SupplierData) => d.amount" :color="supplierChartConfig.amount.color"
|
value: profitMetrics.items_per_transaction,
|
||||||
:rounded-corners="4" bar-padding="0.1" />
|
},
|
||||||
<VisAxis type="x" :x="(_d: SupplierData, i: number) => i" :tick-line="false"
|
{
|
||||||
:domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
label: 'Rata-rata/Transaksi',
|
||||||
supplierBarData[d]?.name ?? ''
|
value: 'Rp' + formatRupiah(profitMetrics.aov),
|
||||||
" :tick-values="supplierBarData.map((_, i) => i)
|
},
|
||||||
" />
|
]" :style="{ order: sectionOrder.totalOrder ?? 99 }" />
|
||||||
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" :tick-format="(d: number) =>
|
|
||||||
'Rp' + formatRupiahShort(d)
|
|
||||||
" />
|
|
||||||
<VisTooltip :triggers="{
|
|
||||||
[barSelector]: supplierTooltip,
|
|
||||||
}" class-name="custom-tooltip" />
|
|
||||||
</VisXYContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
|
||||||
Belum ada data supplier
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card v-if="can('analysis.top_customers')">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-base">Top 5 Pelanggan</CardTitle>
|
|
||||||
<CardDescription>Berdasarkan total nilai pesanan</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer v-if="customerBarData.length > 0" :config="customerChartConfig"
|
|
||||||
class="aspect-auto h-[250px] w-full">
|
|
||||||
<VisXYContainer :data="customerBarData" :y-domain="[0, undefined]">
|
|
||||||
<VisGroupedBar :x="(_d: CustomerData, i: number) => i"
|
|
||||||
:y="(d: CustomerData) => d.amount" :color="customerChartConfig.amount.color"
|
|
||||||
:rounded-corners="4" bar-padding="0.1" />
|
|
||||||
<VisAxis type="x" :x="(_d: CustomerData, i: number) => i" :tick-line="false"
|
|
||||||
:domain-line="false" :grid-line="false" :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' + formatRupiahShort(d)
|
|
||||||
" />
|
|
||||||
<VisTooltip :triggers="{
|
|
||||||
[barSelector]: customerTooltip,
|
|
||||||
}" class-name="custom-tooltip" />
|
|
||||||
</VisXYContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
|
||||||
Belum ada data pelanggan
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card v-if="can('analysis.top_products')">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle class="text-base">Top 5 Produk</CardTitle>
|
|
||||||
<CardDescription>Berdasarkan jumlah terjual</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ChartContainer v-if="productBarData.length > 0" :config="productChartConfig"
|
|
||||||
class="aspect-auto h-[250px] w-full">
|
|
||||||
<VisXYContainer :data="productBarData" :y-domain="[0, undefined]">
|
|
||||||
<VisGroupedBar :x="(_d: ProductData, i: number) => i" :y="(d: ProductData) => d.qty"
|
|
||||||
:color="productChartConfig.qty.color" :rounded-corners="4" bar-padding="0.1" />
|
|
||||||
<VisAxis type="x" :x="(_d: ProductData, i: number) => i" :tick-line="false"
|
|
||||||
:domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
|
||||||
productBarData[d]?.name ?? ''
|
|
||||||
" :tick-values="productBarData.map((_, i) => i)
|
|
||||||
" />
|
|
||||||
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" />
|
|
||||||
<VisTooltip :triggers="{
|
|
||||||
[barSelector]: productTooltip,
|
|
||||||
}" class-name="custom-tooltip" />
|
|
||||||
</VisXYContainer>
|
|
||||||
</ChartContainer>
|
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
|
||||||
Belum ada data produk
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Marketing Sales Table -->
|
<!-- Marketing Sales Table -->
|
||||||
<Card v-if="can('analysis.marketing_sales')">
|
<Card v-if="can('analysis.marketing_sales')" :style="{ order: sectionOrder.marketingSales ?? 99 }">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Penjualan Marketing</CardTitle>
|
<CardTitle>Penjualan Marketing</CardTitle>
|
||||||
<CardDescription>Rekap penjualan per marketing</CardDescription>
|
<CardDescription>Rekap penjualan per marketing</CardDescription>
|
||||||
@ -1092,6 +1001,139 @@ watch([startDate, endDate], () => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- Top 5 Supplier -->
|
||||||
|
<Card v-if="can('analysis.top_suppliers')" :style="{ order: sectionOrder.topSuppliers ?? 99 }">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-base">Top 5 Supplier</CardTitle>
|
||||||
|
<CardDescription>Berdasarkan total harga pembelian</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer v-if="supplierBarData.length > 0" :config="supplierChartConfig"
|
||||||
|
class="aspect-auto h-[250px] w-full">
|
||||||
|
<VisXYContainer :data="supplierBarData" :y-domain="[0, undefined]">
|
||||||
|
<VisGroupedBar :x="(_d: SupplierData, i: number) => i"
|
||||||
|
:y="(d: SupplierData) => d.amount" :color="supplierChartConfig.amount.color"
|
||||||
|
:rounded-corners="4" bar-padding="0.1" />
|
||||||
|
<VisAxis type="x" :x="(_d: SupplierData, i: number) => i" :tick-line="false"
|
||||||
|
:domain-line="false" :grid-line="false" :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' + formatRupiahShort(d)
|
||||||
|
" />
|
||||||
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: supplierTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
|
</VisXYContainer>
|
||||||
|
</ChartContainer>
|
||||||
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
|
Belum ada data supplier
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Top 5 Produk -->
|
||||||
|
<Card v-if="can('analysis.top_products')" :style="{ order: sectionOrder.topProducts ?? 99 }">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-base">Top 5 Produk</CardTitle>
|
||||||
|
<CardDescription>Berdasarkan jumlah terjual</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer v-if="productBarData.length > 0" :config="productChartConfig"
|
||||||
|
class="aspect-auto h-[250px] w-full">
|
||||||
|
<VisXYContainer :data="productBarData" :y-domain="[0, undefined]">
|
||||||
|
<VisGroupedBar :x="(_d: ProductData, i: number) => i" :y="(d: ProductData) => d.qty"
|
||||||
|
:color="productChartConfig.qty.color" :rounded-corners="4" bar-padding="0.1" />
|
||||||
|
<VisAxis type="x" :x="(_d: ProductData, i: number) => i" :tick-line="false"
|
||||||
|
:domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||||
|
productBarData[d]?.name ?? ''
|
||||||
|
" :tick-values="productBarData.map((_, i) => i)
|
||||||
|
" />
|
||||||
|
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" />
|
||||||
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: productTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
|
</VisXYContainer>
|
||||||
|
</ChartContainer>
|
||||||
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
|
Belum ada data produk
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Top 5 Pelanggan -->
|
||||||
|
<Card v-if="can('analysis.top_customers')" :style="{ order: sectionOrder.topCustomers ?? 99 }">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle class="text-base">Top 5 Pelanggan</CardTitle>
|
||||||
|
<CardDescription>Berdasarkan total nilai pesanan</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer v-if="customerBarData.length > 0" :config="customerChartConfig"
|
||||||
|
class="aspect-auto h-[250px] w-full">
|
||||||
|
<VisXYContainer :data="customerBarData" :y-domain="[0, undefined]">
|
||||||
|
<VisGroupedBar :x="(_d: CustomerData, i: number) => i"
|
||||||
|
:y="(d: CustomerData) => d.amount" :color="customerChartConfig.amount.color"
|
||||||
|
:rounded-corners="4" bar-padding="0.1" />
|
||||||
|
<VisAxis type="x" :x="(_d: CustomerData, i: number) => i" :tick-line="false"
|
||||||
|
:domain-line="false" :grid-line="false" :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' + formatRupiahShort(d)
|
||||||
|
" />
|
||||||
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: customerTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
|
</VisXYContainer>
|
||||||
|
</ChartContainer>
|
||||||
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
|
Belum ada data pelanggan
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Busy Hours Chart -->
|
||||||
|
<Card v-if="can('analysis.busy_hours')" :style="{ order: sectionOrder.busyHours ?? 99 }">
|
||||||
|
<CardHeader>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle>Jam Sibuk Toko</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Jam Tersibuk
|
||||||
|
</p>
|
||||||
|
<p class="text-2xl font-bold text-primary">
|
||||||
|
{{ peakHour.hour }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ peakHour.orders }} pesanan
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ChartContainer v-if="busyHours.length > 0" :config="busyHoursChartConfig"
|
||||||
|
class="aspect-auto h-[250px] w-full">
|
||||||
|
<VisXYContainer :data="busyHours" :y-domain="[0, undefined]">
|
||||||
|
<VisGroupedBar :x="(_d: BusyHourData, i: number) => i" :y="(d: BusyHourData) => d.orders"
|
||||||
|
:color="busyHoursChartConfig.orders.color" :bar-padding="0.1" :rounded-corners="4" />
|
||||||
|
<VisAxis type="x" :x="(_d: BusyHourData, i: number) => i" :tick-line="false"
|
||||||
|
:domain-line="false" :grid-line="false" :tick-format="(d: number) => busyHours[d]?.hour ?? ''
|
||||||
|
" :tick-values="busyHourTickValues" />
|
||||||
|
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false" :tick-format="(d: number) => Math.round(d).toString()
|
||||||
|
" />
|
||||||
|
<VisTooltip :triggers="{ [barSelector]: busyHoursTooltip }" class-name="custom-tooltip" />
|
||||||
|
</VisXYContainer>
|
||||||
|
</ChartContainer>
|
||||||
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
|
Belum ada data pesanan
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</AdminLayout>
|
</AdminLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user