refactor: streamline OrderPosCheckoutSection.vue by simplifying component structure and improving readability; enhance code consistency in template syntax
This commit is contained in:
parent
d02af40836
commit
b78d512197
@ -674,20 +674,29 @@ public function getTopProducts(?Carbon $startDate = null, ?Carbon $endDate = nul
|
|||||||
|
|
||||||
public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate = null): array
|
||||||
{
|
{
|
||||||
|
$qtySubquery = DB::table('order_items')
|
||||||
|
->select('order_id', DB::raw('SUM(quantity) as total_qty'))
|
||||||
|
->whereNull('deleted_at')
|
||||||
|
->groupBy('order_id');
|
||||||
|
|
||||||
return Order::query()
|
return Order::query()
|
||||||
->completed()
|
->completed()
|
||||||
->whereNotNull('marketing_id')
|
->whereNotNull('marketing_id')
|
||||||
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
|
->when($startDate && $endDate, fn ($q) => $q->whereBetween('orders.created_at', [$startDate, $endDate]))
|
||||||
->join('users', 'orders.marketing_id', '=', 'users.id')
|
->join('users', 'orders.marketing_id', '=', 'users.id')
|
||||||
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
->leftJoin('user_profiles', 'users.id', '=', 'user_profiles.user_id')
|
||||||
|
->leftJoinSub($qtySubquery, 'order_qtys', function ($join) {
|
||||||
|
$join->on('orders.id', '=', 'order_qtys.order_id');
|
||||||
|
})
|
||||||
->selectRaw('
|
->selectRaw('
|
||||||
users.id as marketing_id,
|
users.id as marketing_id,
|
||||||
COALESCE(user_profiles.full_name, users.username) as marketing_name,
|
COALESCE(user_profiles.full_name, users.username) as marketing_name,
|
||||||
COUNT(*) as total_orders,
|
COUNT(orders.id) as total_orders,
|
||||||
SUM(orders.total_amount) as total_revenue,
|
SUM(orders.total_amount) as total_revenue,
|
||||||
SUM(orders.subtotal) as total_subtotal,
|
SUM(orders.subtotal) as total_subtotal,
|
||||||
SUM(orders.discount) as total_discount,
|
SUM(orders.discount) as total_discount,
|
||||||
AVG(orders.total_amount) as avg_order
|
AVG(orders.total_amount) as avg_order,
|
||||||
|
SUM(COALESCE(order_qtys.total_qty, 0)) as total_products_sold
|
||||||
')
|
')
|
||||||
->groupBy('users.id', 'user_profiles.full_name', 'users.username')
|
->groupBy('users.id', 'user_profiles.full_name', 'users.username')
|
||||||
->orderByDesc('total_revenue')
|
->orderByDesc('total_revenue')
|
||||||
@ -695,6 +704,7 @@ public function getMarketingSales(?Carbon $startDate = null, ?Carbon $endDate =
|
|||||||
->map(fn ($item) => [
|
->map(fn ($item) => [
|
||||||
'marketing_name' => $item->marketing_name,
|
'marketing_name' => $item->marketing_name,
|
||||||
'total_orders' => (int) $item->total_orders,
|
'total_orders' => (int) $item->total_orders,
|
||||||
|
'total_products_sold' => (int) $item->total_products_sold,
|
||||||
'total_revenue' => (int) $item->total_revenue,
|
'total_revenue' => (int) $item->total_revenue,
|
||||||
'total_subtotal' => (int) $item->total_subtotal,
|
'total_subtotal' => (int) $item->total_subtotal,
|
||||||
'total_discount' => (int) $item->total_discount,
|
'total_discount' => (int) $item->total_discount,
|
||||||
|
|||||||
@ -1,8 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Head, router } from '@inertiajs/vue3';
|
import { Head, router } from '@inertiajs/vue3';
|
||||||
import { Banknote, Package, ShoppingCart, TrendingUp, UserCheck } from '@lucide/vue';
|
import {
|
||||||
|
Banknote,
|
||||||
|
Package,
|
||||||
|
ShoppingCart,
|
||||||
|
TrendingUp,
|
||||||
|
UserCheck,
|
||||||
|
} from '@lucide/vue';
|
||||||
import { GroupedBar } from '@unovis/ts';
|
import { GroupedBar } from '@unovis/ts';
|
||||||
import { VisAxis, VisGroupedBar, VisTooltip, VisXYContainer } from '@unovis/vue';
|
import {
|
||||||
|
VisAxis,
|
||||||
|
VisGroupedBar,
|
||||||
|
VisTooltip,
|
||||||
|
VisXYContainer,
|
||||||
|
} from '@unovis/vue';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import StatCard from '@/components/card/StatCard.vue';
|
import StatCard from '@/components/card/StatCard.vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@ -16,7 +27,14 @@ import {
|
|||||||
import type { ChartConfig } from '@/components/ui/chart';
|
import type { ChartConfig } from '@/components/ui/chart';
|
||||||
import { ChartContainer } from '@/components/ui/chart';
|
import { ChartContainer } from '@/components/ui/chart';
|
||||||
import { DatePicker } from '@/components/ui/date-picker';
|
import { DatePicker } from '@/components/ui/date-picker';
|
||||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
import { useCan } from '@/composables/useCan';
|
import { useCan } from '@/composables/useCan';
|
||||||
import AdminLayout from '@/layouts/AdminLayout.vue';
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
||||||
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
import { formatRupiah, formatRupiahShort } from '@/lib/rupiah';
|
||||||
@ -126,6 +144,7 @@ const props = defineProps<{
|
|||||||
marketingSales: Array<{
|
marketingSales: Array<{
|
||||||
marketing_name: string;
|
marketing_name: string;
|
||||||
total_orders: number;
|
total_orders: number;
|
||||||
|
total_products_sold: number;
|
||||||
total_revenue: number;
|
total_revenue: number;
|
||||||
total_subtotal: number;
|
total_subtotal: number;
|
||||||
total_discount: number;
|
total_discount: number;
|
||||||
@ -175,7 +194,12 @@ function onPresetChange(value: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Revenue Chart
|
// Revenue Chart
|
||||||
type MonthlyRevenueData = { month: string; total: number; net: number; deduction: number };
|
type MonthlyRevenueData = {
|
||||||
|
month: string;
|
||||||
|
total: number;
|
||||||
|
net: number;
|
||||||
|
deduction: number;
|
||||||
|
};
|
||||||
|
|
||||||
const revenueChartConfig = {
|
const revenueChartConfig = {
|
||||||
total: {
|
total: {
|
||||||
@ -194,12 +218,21 @@ const revenueChartConfig = {
|
|||||||
|
|
||||||
const revenueTotals = computed(() => ({
|
const revenueTotals = computed(() => ({
|
||||||
total: props.revenueSummary.total_revenue,
|
total: props.revenueSummary.total_revenue,
|
||||||
net: props.revenueSummary.total_revenue - props.revenueSummary.total_deduction - props.profitMetrics.hpp,
|
net:
|
||||||
|
props.revenueSummary.total_revenue -
|
||||||
|
props.revenueSummary.total_deduction -
|
||||||
|
props.profitMetrics.hpp,
|
||||||
deduction: props.revenueSummary.total_deduction,
|
deduction: props.revenueSummary.total_deduction,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Expense Chart
|
// Expense Chart
|
||||||
type MonthlyExpenseData = { month: string; total: number; purchase: number; expense: number; advance: number };
|
type MonthlyExpenseData = {
|
||||||
|
month: string;
|
||||||
|
total: number;
|
||||||
|
purchase: number;
|
||||||
|
expense: number;
|
||||||
|
advance: number;
|
||||||
|
};
|
||||||
|
|
||||||
const expenseChartConfig = {
|
const expenseChartConfig = {
|
||||||
total: {
|
total: {
|
||||||
@ -286,7 +319,10 @@ const peakHour = computed(() => {
|
|||||||
return { hour: '-', orders: 0 };
|
return { hour: '-', orders: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
return props.busyHours.reduce((max, item) => item.orders > max.orders ? item : max, props.busyHours[0]);
|
return props.busyHours.reduce(
|
||||||
|
(max, item) => (item.orders > max.orders ? item : max),
|
||||||
|
props.busyHours[0],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Top 5 Chart configs
|
// Top 5 Chart configs
|
||||||
@ -344,13 +380,16 @@ function revenueTooltip(d: any) {
|
|||||||
function expenseTooltip(d: any) {
|
function expenseTooltip(d: any) {
|
||||||
const item = d as MonthlyExpenseData;
|
const item = d as MonthlyExpenseData;
|
||||||
const charts = visibleExpenseCharts.value;
|
const charts = visibleExpenseCharts.value;
|
||||||
const rows = charts.map((chart) =>
|
const rows = charts
|
||||||
`<p style="margin:0;display:flex;align-items:center;gap:0.5rem">
|
.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="size-2.5 rounded-full" style="background-color: ${expenseChartConfig[chart].color}"></span>
|
||||||
<span class="text-muted-foreground">${expenseChartConfig[chart].label}</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>
|
<span class="ml-auto font-medium tabular-nums text-foreground">Rp${formatRupiah(item[chart])}</span>
|
||||||
</p>`
|
</p>`,
|
||||||
).join('');
|
)
|
||||||
|
.join('');
|
||||||
|
|
||||||
return `<div class="rounded-lg border bg-background px-3 py-1.5 shadow-xl" style="line-height:1.6">
|
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;font-weight:500">${item.month}</p>
|
||||||
@ -470,7 +509,7 @@ watch([startDate, endDate], () => {
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<Select v-model="selectedPreset" @update:model-value="onPresetChange">
|
<Select v-model="selectedPreset" @update:model-value="onPresetChange">
|
||||||
<SelectTrigger class="w-[140px] h-9">
|
<SelectTrigger class="h-9 w-[140px]">
|
||||||
<SelectValue placeholder="Filter Cepat" />
|
<SelectValue placeholder="Filter Cepat" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -516,9 +555,10 @@ watch([startDate, endDate], () => {
|
|||||||
},
|
},
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.attendance') && !isManager && myAttendance" title="Kehadiran Saya"
|
<StatCard v-if="
|
||||||
:icon="UserCheck" main-label="Hari Kerja" :main-value="myAttendance.total_days"
|
can('analysis.attendance') && !isManager && myAttendance
|
||||||
:sub-label="myAttendance.percentage + '% hadir'" :items="[
|
" title="Kehadiran Saya" :icon="UserCheck" main-label="Hari Kerja"
|
||||||
|
:main-value="myAttendance.total_days" :sub-label="myAttendance.percentage + '% hadir'" :items="[
|
||||||
{
|
{
|
||||||
label: 'Hadir',
|
label: 'Hadir',
|
||||||
value: myAttendance.present_days,
|
value: myAttendance.present_days,
|
||||||
@ -534,50 +574,73 @@ watch([startDate, endDate], () => {
|
|||||||
]" />
|
]" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.cash')" title="Kas Toko" :icon="Banknote" main-label="Total Saldo"
|
<StatCard v-if="can('analysis.cash')" title="Kas Toko" :icon="Banknote" main-label="Total Saldo"
|
||||||
:main-value="'Rp' + formatRupiah(cashOverview.total_balance)" :sub-label="cashOverview.total_transactions + ' transaksi'
|
:main-value="'Rp' + formatRupiah(cashOverview.total_balance)
|
||||||
" :items="[
|
" :sub-label="cashOverview.total_transactions + ' transaksi'" :items="[
|
||||||
{
|
{
|
||||||
label: 'Deposit',
|
label: 'Deposit',
|
||||||
value: 'Rp' + formatRupiah(cashOverview.total_deposit),
|
value:
|
||||||
},
|
'Rp' + formatRupiah(cashOverview.total_deposit),
|
||||||
{
|
},
|
||||||
label: 'Withdrawal',
|
{
|
||||||
value: 'Rp' + formatRupiah(cashOverview.total_withdrawal),
|
label: 'Withdrawal',
|
||||||
},
|
value:
|
||||||
]" :cols="2" />
|
'Rp' +
|
||||||
|
formatRupiah(cashOverview.total_withdrawal),
|
||||||
|
},
|
||||||
|
]" :cols="2" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.raw_materials')" title="Bahan Baku" :icon="Package"
|
<StatCard v-if="can('analysis.raw_materials')" title="Bahan Baku" :icon="Package"
|
||||||
main-label="Total Stok" :main-value="rawMaterialStock.total_stock.toLocaleString('id-ID')"
|
main-label="Total Stok" :main-value="rawMaterialStock.total_stock.toLocaleString('id-ID')
|
||||||
:sub-label="'Rp' + formatRupiah(rawMaterialStock.total_value)" :items="[
|
" :sub-label="'Rp' + formatRupiah(rawMaterialStock.total_value)
|
||||||
|
" :items="[
|
||||||
{
|
{
|
||||||
label: 'Yard',
|
label: 'Yard',
|
||||||
value: rawMaterialStock.by_unit?.yard?.toLocaleString('id-ID') ?? '0',
|
value:
|
||||||
|
rawMaterialStock.by_unit?.yard?.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
) ?? '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Meter',
|
label: 'Meter',
|
||||||
value: rawMaterialStock.by_unit?.meter?.toLocaleString('id-ID') ?? '0',
|
value:
|
||||||
|
rawMaterialStock.by_unit?.meter?.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
) ?? '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kg',
|
label: 'Kg',
|
||||||
value: rawMaterialStock.by_unit?.kilogram?.toLocaleString('id-ID') ?? '0',
|
value:
|
||||||
|
rawMaterialStock.by_unit?.kilogram?.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
) ?? '0',
|
||||||
},
|
},
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.product_stock')" title="Stok Produk" :icon="ShoppingCart"
|
<StatCard v-if="can('analysis.product_stock')" title="Stok Produk" :icon="ShoppingCart"
|
||||||
main-label="Total Stok" :main-value="productStock.total_stock.toLocaleString('id-ID')"
|
main-label="Total Stok" :main-value="productStock.total_stock.toLocaleString('id-ID')
|
||||||
:sub-label="'Rp' + formatRupiah(productStock.total_value) + (productStock.total_reject > 0 ? ' (' + productStock.total_reject + ' reject)' : '')"
|
" :sub-label="'Rp' +
|
||||||
:items="[
|
formatRupiah(productStock.total_value) +
|
||||||
|
(productStock.total_reject > 0
|
||||||
|
? ' (' + productStock.total_reject + ' reject)'
|
||||||
|
: '')
|
||||||
|
" :items="[
|
||||||
{
|
{
|
||||||
label: 'Produk',
|
label: 'Produk',
|
||||||
value: productStock.total_products.toLocaleString('id-ID'),
|
value: productStock.total_products.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Varian',
|
label: 'Varian',
|
||||||
value: productStock.total_variants.toLocaleString('id-ID'),
|
value: productStock.total_variants.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Kategori',
|
label: 'Kategori',
|
||||||
value: productStock.total_categories.toLocaleString('id-ID'),
|
value: productStock.total_categories.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
]" />
|
]" />
|
||||||
</div>
|
</div>
|
||||||
@ -589,9 +652,13 @@ watch([startDate, endDate], () => {
|
|||||||
<CardTitle>Pendapatan</CardTitle>
|
<CardTitle>Pendapatan</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div v-for="chart in ['total', 'net', 'deduction'] as const" :key="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">
|
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">
|
<span class="text-xs text-muted-foreground">
|
||||||
{{ revenueChartConfig[chart].label }}
|
{{ revenueChartConfig[chart].label }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
@ -603,31 +670,43 @@ watch([startDate, endDate], () => {
|
|||||||
<CardContent class="px-2 sm:p-6">
|
<CardContent class="px-2 sm:p-6">
|
||||||
<div v-if="monthlyRevenue.length > 0">
|
<div v-if="monthlyRevenue.length > 0">
|
||||||
<div class="mb-3 flex flex-wrap items-center justify-center gap-4">
|
<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"
|
<div v-for="chart in [
|
||||||
class="flex items-center gap-1.5">
|
'total',
|
||||||
<span class="size-2.5 rounded-full"
|
'net',
|
||||||
:style="{ backgroundColor: revenueChartConfig[chart].color }"></span>
|
'deduction',
|
||||||
<span class="text-xs text-muted-foreground">{{ revenueChartConfig[chart].label }}</span>
|
] 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>
|
||||||
</div>
|
</div>
|
||||||
<ChartContainer :config="revenueChartConfig" class="aspect-auto h-[300px] w-full">
|
<ChartContainer :config="revenueChartConfig" class="aspect-auto h-[300px] w-full">
|
||||||
<VisXYContainer :data="monthlyRevenue" :y-domain="[0, undefined]">
|
<VisXYContainer :data="monthlyRevenue" :y-domain="[0, undefined]">
|
||||||
<VisGroupedBar :x="(_d: MonthlyRevenueData, i: number) => i" :y="[
|
<VisGroupedBar :x="(_d: MonthlyRevenueData, i: number) => i
|
||||||
(d: MonthlyRevenueData) => d.total,
|
" :y="[
|
||||||
(d: MonthlyRevenueData) => d.net,
|
(d: MonthlyRevenueData) => d.total,
|
||||||
(d: MonthlyRevenueData) => d.deduction,
|
(d: MonthlyRevenueData) => d.net,
|
||||||
]" :color="[
|
(d: MonthlyRevenueData) => d.deduction,
|
||||||
revenueChartConfig.total.color,
|
]" :color="[
|
||||||
revenueChartConfig.net.color,
|
revenueChartConfig.total.color,
|
||||||
revenueChartConfig.deduction.color,
|
revenueChartConfig.net.color,
|
||||||
]" :bar-padding="0.1" :group-padding="0.2" :rounded-corners="4" />
|
revenueChartConfig.deduction.color,
|
||||||
<VisAxis type="x" :x="(_d: MonthlyRevenueData, i: number) => i" :tick-line="false"
|
]" :bar-padding="0.1" :group-padding="0.2" :rounded-corners="4" />
|
||||||
:domain-line="false" :grid-line="false"
|
<VisAxis type="x" :x="(_d: MonthlyRevenueData, i: number) => i
|
||||||
:tick-format="(d: number) => monthlyRevenue[d]?.month ?? ''"
|
" :tick-line="false" :domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||||
:tick-values="monthlyRevenue.map((_, i) => i)" />
|
monthlyRevenue[d]?.month ?? ''
|
||||||
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false"
|
" :tick-values="monthlyRevenue.map((_, i) => i)
|
||||||
:tick-format="(d: number) => 'Rp' + formatRupiahShort(d)" />
|
" />
|
||||||
<VisTooltip :triggers="{ [barSelector]: revenueTooltip }" class-name="custom-tooltip" />
|
<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>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -646,7 +725,7 @@ watch([startDate, endDate], () => {
|
|||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div v-for="chart in visibleExpenseCharts" :key="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">
|
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">
|
<span class="text-xs text-muted-foreground">
|
||||||
{{ expenseChartConfig[chart].label }}
|
{{ expenseChartConfig[chart].label }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
@ -659,23 +738,31 @@ watch([startDate, endDate], () => {
|
|||||||
<div v-if="monthlyExpense.length > 0">
|
<div v-if="monthlyExpense.length > 0">
|
||||||
<div class="mb-3 flex flex-wrap items-center justify-center gap-4">
|
<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">
|
<div v-for="chart in visibleExpenseCharts" :key="chart" class="flex items-center gap-1.5">
|
||||||
<span class="size-2.5 rounded-full"
|
<span class="size-2.5 rounded-full" :style="{
|
||||||
:style="{ backgroundColor: expenseChartConfig[chart].color }"></span>
|
backgroundColor:
|
||||||
<span class="text-xs text-muted-foreground">{{ expenseChartConfig[chart].label }}</span>
|
expenseChartConfig[chart].color,
|
||||||
|
}"></span>
|
||||||
|
<span class="text-xs text-muted-foreground">{{
|
||||||
|
expenseChartConfig[chart].label
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChartContainer :config="expenseChartConfig" class="aspect-auto h-[300px] w-full">
|
<ChartContainer :config="expenseChartConfig" class="aspect-auto h-[300px] w-full">
|
||||||
<VisXYContainer :data="monthlyExpense" :y-domain="[0, undefined]">
|
<VisXYContainer :data="monthlyExpense" :y-domain="[0, undefined]">
|
||||||
<VisGroupedBar :x="(_d: MonthlyExpenseData, i: number) => i" :y="expenseYAccessors"
|
<VisGroupedBar :x="(_d: MonthlyExpenseData, i: number) => i
|
||||||
:color="expenseColors" :bar-padding="0.1" :group-padding="0.2"
|
" :y="expenseYAccessors" :color="expenseColors" :bar-padding="0.1"
|
||||||
:rounded-corners="4" />
|
:group-padding="0.2" :rounded-corners="4" />
|
||||||
<VisAxis type="x" :x="(_d: MonthlyExpenseData, i: number) => i" :tick-line="false"
|
<VisAxis type="x" :x="(_d: MonthlyExpenseData, i: number) => i
|
||||||
:domain-line="false" :grid-line="false"
|
" :tick-line="false" :domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||||
:tick-format="(d: number) => monthlyExpense[d]?.month ?? ''"
|
monthlyExpense[d]?.month ?? ''
|
||||||
:tick-values="monthlyExpense.map((_, i) => i)" />
|
" :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)" />
|
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false" :tick-format="(d: number) =>
|
||||||
<VisTooltip :triggers="{ [barSelector]: expenseTooltip }" class-name="custom-tooltip" />
|
'Rp' + formatRupiahShort(d)
|
||||||
|
" />
|
||||||
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: expenseTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
@ -693,9 +780,15 @@ watch([startDate, endDate], () => {
|
|||||||
<CardTitle>Jam Sibuk Toko</CardTitle>
|
<CardTitle>Jam Sibuk Toko</CardTitle>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-sm text-muted-foreground">Jam Tersibuk</p>
|
<p class="text-sm text-muted-foreground">
|
||||||
<p class="text-2xl font-bold text-primary">{{ peakHour.hour }}</p>
|
Jam Tersibuk
|
||||||
<p class="text-xs text-muted-foreground">{{ peakHour.orders }} pesanan</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@ -706,11 +799,10 @@ watch([startDate, endDate], () => {
|
|||||||
<VisGroupedBar :x="(_d: BusyHourData, i: number) => i" :y="(d: BusyHourData) => d.orders"
|
<VisGroupedBar :x="(_d: BusyHourData, i: number) => i" :y="(d: BusyHourData) => d.orders"
|
||||||
:color="busyHoursChartConfig.orders.color" :bar-padding="0.1" :rounded-corners="4" />
|
:color="busyHoursChartConfig.orders.color" :bar-padding="0.1" :rounded-corners="4" />
|
||||||
<VisAxis type="x" :x="(_d: BusyHourData, i: number) => i" :tick-line="false"
|
<VisAxis type="x" :x="(_d: BusyHourData, i: number) => i" :tick-line="false"
|
||||||
:domain-line="false" :grid-line="false"
|
:domain-line="false" :grid-line="false" :tick-format="(d: number) => busyHours[d]?.hour ?? ''
|
||||||
:tick-format="(d: number) => busyHours[d]?.hour ?? ''"
|
" :tick-values="busyHours.map((_, i) => i)" />
|
||||||
:tick-values="busyHours.map((_, i) => i)" />
|
<VisAxis type="y" :num-ticks="4" :tick-line="false" :domain-line="false" :tick-format="(d: number) => Math.round(d).toString()
|
||||||
<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" />
|
<VisTooltip :triggers="{ [barSelector]: busyHoursTooltip }" class-name="custom-tooltip" />
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
@ -723,11 +815,13 @@ watch([startDate, endDate], () => {
|
|||||||
<!-- Profit Metrics Cards -->
|
<!-- Profit Metrics Cards -->
|
||||||
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
<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"
|
<StatCard v-if="can('analysis.profit_orders')" title="Total Order" :icon="ShoppingCart"
|
||||||
main-label="Pesanan Selesai" :main-value="profitMetrics.total_orders.toLocaleString('id-ID')"
|
main-label="Pesanan Selesai" :main-value="profitMetrics.total_orders.toLocaleString('id-ID')
|
||||||
:items="[
|
" :items="[
|
||||||
{
|
{
|
||||||
label: 'Produk Terjual',
|
label: 'Produk Terjual',
|
||||||
value: profitMetrics.total_products_sold.toLocaleString('id-ID'),
|
value: profitMetrics.total_products_sold.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Item/Transaksi',
|
label: 'Item/Transaksi',
|
||||||
@ -739,12 +833,16 @@ watch([startDate, endDate], () => {
|
|||||||
},
|
},
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
<StatCard v-if="can('analysis.profit_gross') || can('analysis.profit_hpp')" title="Laba Kotor"
|
<StatCard v-if="
|
||||||
:icon="TrendingUp" main-label="Laba Kotor"
|
can('analysis.profit_gross') ||
|
||||||
:main-value="'Rp' + formatRupiah(profitMetrics.gross_profit)" :items="[
|
can('analysis.profit_hpp')
|
||||||
|
" title="Laba Kotor" :icon="TrendingUp" main-label="Laba Kotor" :main-value="'Rp' + formatRupiah(profitMetrics.gross_profit)
|
||||||
|
" :items="[
|
||||||
{
|
{
|
||||||
label: 'Pendapatan',
|
label: 'Pendapatan',
|
||||||
value: 'Rp' + formatRupiah(revenueSummary.total_revenue),
|
value:
|
||||||
|
'Rp' +
|
||||||
|
formatRupiah(revenueSummary.total_revenue),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'HPP',
|
label: 'HPP',
|
||||||
@ -752,7 +850,12 @@ watch([startDate, endDate], () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Laba Bersih',
|
label: 'Laba Bersih',
|
||||||
value: 'Rp' + formatRupiah(profitMetrics.net_profit) + ' (' + profitMetrics.profit_margin + '%)',
|
value:
|
||||||
|
'Rp' +
|
||||||
|
formatRupiah(profitMetrics.net_profit) +
|
||||||
|
' (' +
|
||||||
|
profitMetrics.profit_margin +
|
||||||
|
'%)',
|
||||||
},
|
},
|
||||||
]" />
|
]" />
|
||||||
</div>
|
</div>
|
||||||
@ -772,13 +875,16 @@ watch([startDate, endDate], () => {
|
|||||||
:y="(d: SupplierData) => d.amount" :color="supplierChartConfig.amount.color"
|
:y="(d: SupplierData) => d.amount" :color="supplierChartConfig.amount.color"
|
||||||
:rounded-corners="4" bar-padding="0.1" />
|
:rounded-corners="4" bar-padding="0.1" />
|
||||||
<VisAxis type="x" :x="(_d: SupplierData, i: number) => i" :tick-line="false"
|
<VisAxis type="x" :x="(_d: SupplierData, i: number) => i" :tick-line="false"
|
||||||
:domain-line="false" :grid-line="false"
|
:domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||||
:tick-format="(d: number) => supplierBarData[d]?.name ?? ''"
|
supplierBarData[d]?.name ?? ''
|
||||||
:tick-values="supplierBarData.map((_, i) => i)" />
|
" :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)" />
|
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" :tick-format="(d: number) =>
|
||||||
<VisTooltip :triggers="{ [barSelector]: supplierTooltip }"
|
'Rp' + formatRupiahShort(d)
|
||||||
class-name="custom-tooltip" />
|
" />
|
||||||
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: supplierTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
@ -800,13 +906,16 @@ watch([startDate, endDate], () => {
|
|||||||
:y="(d: CustomerData) => d.amount" :color="customerChartConfig.amount.color"
|
:y="(d: CustomerData) => d.amount" :color="customerChartConfig.amount.color"
|
||||||
:rounded-corners="4" bar-padding="0.1" />
|
:rounded-corners="4" bar-padding="0.1" />
|
||||||
<VisAxis type="x" :x="(_d: CustomerData, i: number) => i" :tick-line="false"
|
<VisAxis type="x" :x="(_d: CustomerData, i: number) => i" :tick-line="false"
|
||||||
:domain-line="false" :grid-line="false"
|
:domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||||
:tick-format="(d: number) => customerBarData[d]?.name ?? ''"
|
customerBarData[d]?.name ?? ''
|
||||||
:tick-values="customerBarData.map((_, i) => i)" />
|
" :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)" />
|
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" :tick-format="(d: number) =>
|
||||||
<VisTooltip :triggers="{ [barSelector]: customerTooltip }"
|
'Rp' + formatRupiahShort(d)
|
||||||
class-name="custom-tooltip" />
|
" />
|
||||||
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: customerTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
@ -827,11 +936,14 @@ watch([startDate, endDate], () => {
|
|||||||
<VisGroupedBar :x="(_d: ProductData, i: number) => i" :y="(d: ProductData) => d.qty"
|
<VisGroupedBar :x="(_d: ProductData, i: number) => i" :y="(d: ProductData) => d.qty"
|
||||||
:color="productChartConfig.qty.color" :rounded-corners="4" bar-padding="0.1" />
|
:color="productChartConfig.qty.color" :rounded-corners="4" bar-padding="0.1" />
|
||||||
<VisAxis type="x" :x="(_d: ProductData, i: number) => i" :tick-line="false"
|
<VisAxis type="x" :x="(_d: ProductData, i: number) => i" :tick-line="false"
|
||||||
:domain-line="false" :grid-line="false"
|
:domain-line="false" :grid-line="false" :tick-format="(d: number) =>
|
||||||
:tick-format="(d: number) => productBarData[d]?.name ?? ''"
|
productBarData[d]?.name ?? ''
|
||||||
:tick-values="productBarData.map((_, i) => i)" />
|
" :tick-values="productBarData.map((_, i) => i)
|
||||||
|
" />
|
||||||
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" />
|
<VisAxis type="y" :num-ticks="3" :tick-line="false" :domain-line="false" />
|
||||||
<VisTooltip :triggers="{ [barSelector]: productTooltip }" class-name="custom-tooltip" />
|
<VisTooltip :triggers="{
|
||||||
|
[barSelector]: productTooltip,
|
||||||
|
}" class-name="custom-tooltip" />
|
||||||
</VisXYContainer>
|
</VisXYContainer>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
<div v-else class="flex h-[250px] items-center justify-center text-muted-foreground">
|
||||||
@ -852,22 +964,65 @@ watch([startDate, endDate], () => {
|
|||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="border-b">
|
<tr class="border-b">
|
||||||
<th class="py-3 px-4 text-left font-medium text-muted-foreground">Marketing</th>
|
<th class="px-4 py-3 text-left font-medium text-muted-foreground">
|
||||||
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Order</th>
|
Marketing
|
||||||
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Pendapatan</th>
|
</th>
|
||||||
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Subtotal</th>
|
<th class="px-4 py-3 text-right font-medium text-muted-foreground">
|
||||||
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Total Diskon</th>
|
Total Order
|
||||||
<th class="py-3 px-4 text-right font-medium text-muted-foreground">Rata-rata Order</th>
|
</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium text-muted-foreground">
|
||||||
|
Produk Terjual
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium text-muted-foreground">
|
||||||
|
Total Pendapatan
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium text-muted-foreground">
|
||||||
|
Total Subtotal
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium text-muted-foreground">
|
||||||
|
Total Diskon
|
||||||
|
</th>
|
||||||
|
<th class="px-4 py-3 text-right font-medium text-muted-foreground">
|
||||||
|
Rata-rata Order
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(item, index) in marketingSales" :key="index" class="border-b last:border-0">
|
<tr v-for="(item, index) in marketingSales" :key="index" class="border-b last:border-0">
|
||||||
<td class="py-3 px-4 font-medium">{{ item.marketing_name }}</td>
|
<td class="px-4 py-3 font-medium">
|
||||||
<td class="py-3 px-4 text-right tabular-nums">{{ item.total_orders.toLocaleString('id-ID') }}</td>
|
{{ item.marketing_name }}
|
||||||
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.total_revenue) }}</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.total_subtotal) }}</td>
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.total_discount) }}</td>
|
{{
|
||||||
<td class="py-3 px-4 text-right tabular-nums">Rp{{ formatRupiah(item.avg_order) }}</td>
|
item.total_orders.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
|
{{
|
||||||
|
item.total_products_sold.toLocaleString(
|
||||||
|
'id-ID',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
pcs
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
|
Rp{{ formatRupiah(item.total_revenue) }}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
|
Rp{{
|
||||||
|
formatRupiah(item.total_subtotal)
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
|
Rp{{
|
||||||
|
formatRupiah(item.total_discount)
|
||||||
|
}}
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right tabular-nums">
|
||||||
|
Rp{{ formatRupiah(item.avg_order) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user