store/resources/js/pages/admin/Analysis.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>