444 lines
15 KiB
Vue
444 lines
15 KiB
Vue
<script setup lang="ts">
|
|
import LowStockCard from '@/components/card/LowStockCard.vue';
|
|
import StatCard from '@/components/card/StatCard.vue';
|
|
import { DatePicker } from '@/components/ui/date-picker';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardDescription,
|
|
} from '@/components/ui/card';
|
|
import AdminLayout from '@/layouts/AdminLayout.vue';
|
|
import { Head, router } from '@inertiajs/vue3';
|
|
import { computed, ref, watch } from 'vue';
|
|
import admin from '@/routes/admin';
|
|
import {
|
|
AlertTriangle,
|
|
Banknote,
|
|
Clock,
|
|
CreditCard,
|
|
FileText,
|
|
Package,
|
|
ShoppingCart,
|
|
TrendingDown,
|
|
TrendingUp,
|
|
UserCheck,
|
|
Calendar,
|
|
} from '@lucide/vue';
|
|
|
|
const props = defineProps<{
|
|
filters: {
|
|
start_date?: string;
|
|
end_date?: string;
|
|
};
|
|
rawMaterialStock: {
|
|
total_stock: number;
|
|
total_value: number;
|
|
by_unit: Record<string, number>;
|
|
};
|
|
productStock: {
|
|
total_stock: number;
|
|
total_reject: number;
|
|
total_value: number;
|
|
total_variants: number;
|
|
total_products: number;
|
|
total_categories: number;
|
|
};
|
|
lowStockProducts: Array<{
|
|
name: string;
|
|
stock: number;
|
|
}>;
|
|
lowStockMaterials: Array<{
|
|
name: string;
|
|
stock: number;
|
|
unit: string;
|
|
}>;
|
|
purchaseSummary: {
|
|
total_purchases: number;
|
|
total_spent: number;
|
|
total_discount: number;
|
|
};
|
|
revenueSummary: {
|
|
total_revenue: number;
|
|
total_discount: number;
|
|
total_marketplace_fees: number;
|
|
total_potongan: number;
|
|
total_shipping: number;
|
|
total_orders: number;
|
|
avg_order: number;
|
|
};
|
|
cashAccounts: Array<{
|
|
name: string;
|
|
balance: number;
|
|
}>;
|
|
cashSummary: {
|
|
total_transactions: number;
|
|
total_deposit: number;
|
|
total_withdrawal: number;
|
|
};
|
|
monthlyExpenses: {
|
|
total: number;
|
|
count: number;
|
|
};
|
|
kasbonSummary: {
|
|
pending: { count: number; total: number };
|
|
approved: { count: number; total: number };
|
|
paid: { count: number; total: number };
|
|
total: number;
|
|
total_employees: number;
|
|
};
|
|
leaveRequestSummary: {
|
|
total: number;
|
|
pending: number;
|
|
approved: number;
|
|
rejected: number;
|
|
};
|
|
attendanceToday: {
|
|
total_employees: number;
|
|
present: number;
|
|
absent: number;
|
|
on_leave: number;
|
|
};
|
|
}>();
|
|
|
|
const startDate = ref(props.filters.start_date ?? '');
|
|
const endDate = ref(props.filters.end_date ?? '');
|
|
|
|
const hasActiveFilters = computed(() => !!startDate.value || !!endDate.value);
|
|
|
|
function formatRupiah(value: number): string {
|
|
return 'Rp ' + value.toLocaleString('id-ID');
|
|
}
|
|
|
|
const totalCashBalance = computed(() => {
|
|
return props.cashAccounts.reduce((sum, acc) => sum + acc.balance, 0);
|
|
});
|
|
|
|
function applyFilters() {
|
|
router.get(
|
|
admin.analysis.url({
|
|
start_date: startDate.value,
|
|
end_date: endDate.value,
|
|
}),
|
|
{},
|
|
{
|
|
preserveState: true,
|
|
preserveScroll: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
function clearFilters() {
|
|
startDate.value = '';
|
|
endDate.value = '';
|
|
applyFilters();
|
|
}
|
|
|
|
watch([startDate, endDate], () => {
|
|
applyFilters();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Head title="Analisa" />
|
|
|
|
<AdminLayout>
|
|
<div class="flex flex-col gap-6">
|
|
<!-- Header & Filter -->
|
|
<div
|
|
class="flex flex-col gap-4 rounded-lg border bg-card p-6 shadow-xs md:flex-row md:items-center md:justify-between"
|
|
>
|
|
<div class="space-y-1">
|
|
<h2 class="text-2xl font-bold tracking-tight">Analisa</h2>
|
|
<p class="text-sm text-muted-foreground">
|
|
Halaman analisa data, performa toko, dan inventori.
|
|
</p>
|
|
</div>
|
|
<!-- Date Filters -->
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
class="text-xs font-semibold tracking-wider text-muted-foreground uppercase"
|
|
>Mulai:</span
|
|
>
|
|
<DatePicker
|
|
v-model="startDate"
|
|
class="w-[170px]"
|
|
placeholder="Pilih tanggal"
|
|
/>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<span
|
|
class="text-xs font-semibold tracking-wider text-muted-foreground uppercase"
|
|
>Sampai:</span
|
|
>
|
|
<DatePicker
|
|
v-model="endDate"
|
|
class="w-[170px]"
|
|
placeholder="Pilih tanggal"
|
|
/>
|
|
</div>
|
|
<Button
|
|
v-if="hasActiveFilters"
|
|
variant="ghost"
|
|
size="sm"
|
|
@click="clearFilters"
|
|
class="h-9 px-3"
|
|
>
|
|
Reset Filter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 1: KPI Statistic Cards -->
|
|
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<!-- 1. Total Pendapatan -->
|
|
<StatCard
|
|
title="Total Pendapatan"
|
|
:icon="TrendingUp"
|
|
main-label="Total"
|
|
:main-value="formatRupiah(revenueSummary.total_revenue)"
|
|
:sub-label="
|
|
revenueSummary.total_orders +
|
|
' transaksi selesai' +
|
|
(hasActiveFilters ? ' (filter)' : ' (keseluruhan)')
|
|
"
|
|
:items="[
|
|
{
|
|
label: 'Bersih',
|
|
value: formatRupiah(
|
|
revenueSummary.total_revenue -
|
|
revenueSummary.total_potongan,
|
|
),
|
|
},
|
|
{
|
|
label: 'Potongan',
|
|
value: formatRupiah(revenueSummary.total_potongan),
|
|
},
|
|
{
|
|
label: 'Diskon',
|
|
value: formatRupiah(revenueSummary.total_discount),
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<!-- 2. Total Pengeluaran -->
|
|
<StatCard
|
|
title="Total Pengeluaran"
|
|
:icon="TrendingDown"
|
|
main-label="Total"
|
|
:main-value="
|
|
formatRupiah(
|
|
purchaseSummary.total_spent +
|
|
monthlyExpenses.total +
|
|
kasbonSummary.total,
|
|
)
|
|
"
|
|
:items="[
|
|
{
|
|
label: 'Belanja',
|
|
value: formatRupiah(purchaseSummary.total_spent),
|
|
},
|
|
{
|
|
label: 'Pengeluaran',
|
|
value: formatRupiah(monthlyExpenses.total),
|
|
},
|
|
{
|
|
label: 'Kasbon',
|
|
value: formatRupiah(kasbonSummary.total),
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<!-- 3. Kas Toko -->
|
|
<StatCard
|
|
title="Kas Toko"
|
|
:icon="Banknote"
|
|
main-label="Total Saldo"
|
|
:main-value="formatRupiah(totalCashBalance)"
|
|
:sub-label="
|
|
cashSummary.total_transactions +
|
|
' transaksi' +
|
|
(hasActiveFilters ? ' (filter)' : ' (keseluruhan)')
|
|
"
|
|
:items="[
|
|
{
|
|
label: 'Deposit',
|
|
value: formatRupiah(cashSummary.total_deposit),
|
|
},
|
|
{
|
|
label: 'Withdrawal',
|
|
value: formatRupiah(cashSummary.total_withdrawal),
|
|
},
|
|
]"
|
|
:cols="2"
|
|
/>
|
|
|
|
<!-- 4. Kehadiran -->
|
|
<StatCard
|
|
title="Kehadiran"
|
|
:icon="UserCheck"
|
|
main-label="Slot Kehadiran"
|
|
:main-value="attendanceToday.total_employees"
|
|
:sub-label="
|
|
(attendanceToday.total_employees > 0
|
|
? Math.round(
|
|
(attendanceToday.present /
|
|
attendanceToday.total_employees) *
|
|
100,
|
|
)
|
|
: 0) + '% hadir'
|
|
"
|
|
:items="[
|
|
{
|
|
label: 'Hadir',
|
|
value: attendanceToday.present,
|
|
},
|
|
{
|
|
label: 'Tidak Hadir',
|
|
value: attendanceToday.absent,
|
|
},
|
|
{
|
|
label: 'Cuti',
|
|
value: attendanceToday.on_leave ?? 0,
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<!-- 5. Bahan Baku -->
|
|
<StatCard
|
|
title="Bahan Baku"
|
|
:icon="Package"
|
|
main-label="Total Stok"
|
|
:main-value="
|
|
rawMaterialStock.total_stock.toLocaleString('id-ID')
|
|
"
|
|
:sub-label="formatRupiah(rawMaterialStock.total_value)"
|
|
:items="[
|
|
{
|
|
label: 'Yard',
|
|
value: (
|
|
rawMaterialStock.by_unit?.yard ?? 0
|
|
).toLocaleString('id-ID'),
|
|
},
|
|
{
|
|
label: 'Meter',
|
|
value: (
|
|
rawMaterialStock.by_unit?.meter ?? 0
|
|
).toLocaleString('id-ID'),
|
|
},
|
|
{
|
|
label: 'Kg',
|
|
value: (
|
|
rawMaterialStock.by_unit?.kilogram ?? 0
|
|
).toLocaleString('id-ID'),
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<!-- 6. Stok Produk -->
|
|
<StatCard
|
|
title="Stok Produk"
|
|
:icon="ShoppingCart"
|
|
main-label="Total Stok"
|
|
:main-value="
|
|
productStock.total_stock.toLocaleString('id-ID')
|
|
"
|
|
:sub-label="
|
|
formatRupiah(productStock.total_value) +
|
|
(productStock.total_reject > 0
|
|
? ` (${productStock.total_reject} reject)`
|
|
: '')
|
|
"
|
|
:items="[
|
|
{
|
|
label: 'Produk',
|
|
value: productStock.total_products.toLocaleString(
|
|
'id-ID',
|
|
),
|
|
},
|
|
{
|
|
label: 'Varian',
|
|
value: productStock.total_variants.toLocaleString(
|
|
'id-ID',
|
|
),
|
|
},
|
|
{
|
|
label: 'Kategori',
|
|
value: productStock.total_categories.toLocaleString(
|
|
'id-ID',
|
|
),
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<!-- 7. Kasbon Karyawan -->
|
|
<StatCard
|
|
title="Kasbon Karyawan"
|
|
:icon="CreditCard"
|
|
main-label="Total Kasbon"
|
|
:main-value="formatRupiah(kasbonSummary.total)"
|
|
:sub-label="kasbonSummary.total_employees + ' karyawan'"
|
|
:items="[
|
|
{
|
|
label: 'Menunggu',
|
|
value: formatRupiah(kasbonSummary.pending.total),
|
|
},
|
|
{
|
|
label: 'Disetujui',
|
|
value: formatRupiah(kasbonSummary.approved.total),
|
|
},
|
|
{
|
|
label: 'Dibayar',
|
|
value: formatRupiah(kasbonSummary.paid.total),
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<!-- 8. Pengajuan Cuti -->
|
|
<StatCard
|
|
title="Pengajuan Cuti"
|
|
:icon="FileText"
|
|
main-label="Total Pengajuan"
|
|
:main-value="leaveRequestSummary.total"
|
|
:items="[
|
|
{
|
|
label: 'Menunggu',
|
|
value: leaveRequestSummary.pending,
|
|
},
|
|
{
|
|
label: 'Disetujui',
|
|
value: leaveRequestSummary.approved,
|
|
},
|
|
{
|
|
label: 'Ditolak',
|
|
value: leaveRequestSummary.rejected,
|
|
},
|
|
]"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Row 2: Low Stock Alerts -->
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
<!-- Stok Produk Menipis -->
|
|
<LowStockCard
|
|
title="Stok Produk Menipis"
|
|
:icon="AlertTriangle"
|
|
:items="lowStockProducts"
|
|
empty-text="Semua stok produk aman"
|
|
/>
|
|
|
|
<!-- Stok Bahan Baku Menipis -->
|
|
<LowStockCard
|
|
title="Stok Bahan Baku Menipis"
|
|
:icon="AlertTriangle"
|
|
:items="lowStockMaterials"
|
|
empty-text="Semua stok bahan baku aman"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
</template>
|